diff --git a/.core_files.yaml b/.core_files.yaml index 1a220eef7a242..ebc3ff376f8ae 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -64,6 +64,7 @@ components: &components - homeassistant/components/group/* - homeassistant/components/hassio/* - homeassistant/components/homeassistant/** + - homeassistant/components/http/** - homeassistant/components/image/* - homeassistant/components/input_boolean/* - homeassistant/components/input_button/* @@ -75,6 +76,7 @@ components: &components - homeassistant/components/logger/* - homeassistant/components/lovelace/* - homeassistant/components/media_source/* + - homeassistant/components/mjpeg/* - homeassistant/components/mqtt/* - homeassistant/components/network/* - homeassistant/components/onboarding/* @@ -101,6 +103,7 @@ components: &components # Testing related files that affect the whole test/linting suite tests: &tests - codecov.yaml + - pylint/** - requirements_test_pre_commit.txt - requirements_test.txt - tests/auth/** @@ -111,6 +114,7 @@ tests: &tests - tests/helpers/* - tests/ignore_uncaught_exceptions.py - tests/mock/* + - tests/pylint/* - tests/scripts/* - tests/test_util/* - tests/testing_config/** @@ -120,15 +124,16 @@ other: &other - .github/workflows/* - homeassistant/scripts/** -requirements: +requirements: &requirements - .github/workflows/* - homeassistant/package_constraints.txt - requirements*.txt - - setup.py + - setup.cfg any: - *base_platforms - *components - *core - *other + - *requirements - *tests diff --git a/.coveragerc b/.coveragerc index a7a6298a69fda..360bd5f6911b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -52,7 +52,9 @@ omit = homeassistant/components/amazon_polly/* homeassistant/components/amberelectric/__init__.py homeassistant/components/ambiclimate/climate.py - homeassistant/components/ambient_station/* + homeassistant/components/ambient_station/__init__.py + homeassistant/components/ambient_station/binary_sensor.py + homeassistant/components/ambient_station/sensor.py homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* @@ -75,11 +77,13 @@ omit = homeassistant/components/aruba/device_tracker.py homeassistant/components/arwn/sensor.py homeassistant/components/aseko_pool_live/__init__.py + homeassistant/components/aseko_pool_live/binary_sensor.py homeassistant/components/aseko_pool_live/entity.py homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/__init__.py + homeassistant/components/asuswrt/diagnostics.py homeassistant/components/asuswrt/router.py homeassistant/components/aten_pe/* homeassistant/components/atome/* @@ -87,6 +91,7 @@ omit = homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py homeassistant/components/aurora/sensor.py + homeassistant/components/aussie_broadband/diagnostics.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_devops/__init__.py @@ -189,7 +194,10 @@ omit = homeassistant/components/crownstone/light.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py - homeassistant/components/daikin/* + homeassistant/components/daikin/__init__.py + homeassistant/components/daikin/climate.py + homeassistant/components/daikin/sensor.py + homeassistant/components/daikin/switch.py homeassistant/components/danfoss_air/* homeassistant/components/darksky/weather.py homeassistant/components/ddwrt/device_tracker.py @@ -222,7 +230,12 @@ omit = homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* homeassistant/components/doods/* - homeassistant/components/doorbird/* + homeassistant/components/doorbird/__init__.py + homeassistant/components/doorbird/button.py + homeassistant/components/doorbird/camera.py + homeassistant/components/doorbird/entity.py + homeassistant/components/doorbird/logbook.py + homeassistant/components/doorbird/util.py homeassistant/components/dovado/* homeassistant/components/downloader/* homeassistant/components/dsmr_reader/* @@ -256,7 +269,14 @@ omit = homeassistant/components/egardia/* homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py - homeassistant/components/elkm1/* + homeassistant/components/elkm1/__init__.py + homeassistant/components/elkm1/alarm_control_panel.py + homeassistant/components/elkm1/climate.py + homeassistant/components/elkm1/discovery.py + homeassistant/components/elkm1/light.py + homeassistant/components/elkm1/scene.py + homeassistant/components/elkm1/sensor.py + homeassistant/components/elkm1/switch.py homeassistant/components/elmax/__init__.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py @@ -300,6 +320,7 @@ omit = homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py + homeassistant/components/esphome/lock.py homeassistant/components/esphome/number.py homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py @@ -339,6 +360,9 @@ omit = homeassistant/components/firmata/sensor.py homeassistant/components/firmata/switch.py homeassistant/components/fitbit/* + homeassistant/components/fivem/__init__.py + homeassistant/components/fivem/binary_sensor.py + homeassistant/components/fivem/sensor.py homeassistant/components/fixer/sensor.py homeassistant/components/fjaraskupan/__init__.py homeassistant/components/fjaraskupan/binary_sensor.py @@ -370,13 +394,10 @@ omit = homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py - homeassistant/components/fritz/__init__.py homeassistant/components/fritz/binary_sensor.py - homeassistant/components/fritz/button.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py - homeassistant/components/fritz/diagnostics.py homeassistant/components/fritz/sensor.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py @@ -400,20 +421,15 @@ omit = homeassistant/components/glances/__init__.py homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py - homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py homeassistant/components/goodwe/const.py homeassistant/components/goodwe/number.py homeassistant/components/goodwe/select.py homeassistant/components/goodwe/sensor.py - homeassistant/components/google/__init__.py homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_pubsub/__init__.py - homeassistant/components/google_travel_time/__init__.py - homeassistant/components/google_travel_time/helpers.py - homeassistant/components/google_travel_time/sensor.py homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py @@ -430,7 +446,11 @@ omit = homeassistant/components/habitica/__init__.py homeassistant/components/habitica/const.py homeassistant/components/habitica/sensor.py - homeassistant/components/hangouts/* + 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 @@ -471,7 +491,12 @@ omit = homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py homeassistant/components/htu21d/sensor.py - homeassistant/components/huawei_lte/* + homeassistant/components/huawei_lte/__init__.py + homeassistant/components/huawei_lte/binary_sensor.py + homeassistant/components/huawei_lte/device_tracker.py + homeassistant/components/huawei_lte/notify.py + homeassistant/components/huawei_lte/sensor.py + homeassistant/components/huawei_lte/switch.py homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/scene.py @@ -497,7 +522,9 @@ omit = homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* - homeassistant/components/ifttt/* + homeassistant/components/ifttt/__init__.py + homeassistant/components/ifttt/alarm_control_panel.py + homeassistant/components/ifttt/const.py homeassistant/components/iglo/light.py homeassistant/components/ihc/* homeassistant/components/imap/sensor.py @@ -517,12 +544,17 @@ omit = homeassistant/components/intellifire/coordinator.py homeassistant/components/intellifire/binary_sensor.py homeassistant/components/intellifire/sensor.py + homeassistant/components/intellifire/entity.py homeassistant/components/incomfort/* homeassistant/components/intesishome/* - homeassistant/components/ios/* + homeassistant/components/ios/__init__.py + homeassistant/components/ios/notify.py + homeassistant/components/ios/sensor.py homeassistant/components/iperf3/* - homeassistant/components/iqvia/* + homeassistant/components/iqvia/__init__.py + homeassistant/components/iqvia/sensor.py homeassistant/components/irish_rail_transport/sensor.py + homeassistant/components/iss/__init__.py homeassistant/components/iss/binary_sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py @@ -569,7 +601,10 @@ omit = homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py - homeassistant/components/konnected/* + homeassistant/components/konnected/__init__.py + homeassistant/components/konnected/handlers.py + homeassistant/components/konnected/panel.py + homeassistant/components/konnected/switch.py homeassistant/components/kostal_plenticore/__init__.py homeassistant/components/kostal_plenticore/const.py homeassistant/components/kostal_plenticore/helper.py @@ -594,8 +629,13 @@ omit = homeassistant/components/lcn/services.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py - homeassistant/components/life360/* - homeassistant/components/lifx/* + homeassistant/components/life360/__init__.py + homeassistant/components/life360/const.py + homeassistant/components/life360/device_tracker.py + homeassistant/components/life360/helpers.py + homeassistant/components/lifx/__init__.py + homeassistant/components/lifx/const.py + homeassistant/components/lifx/light.py homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py @@ -676,9 +716,15 @@ omit = homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py + homeassistant/components/mjpeg/util.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py + homeassistant/components/modbus/binary_sensor.py + homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py + homeassistant/components/moehlenhoff_alpha2/__init__.py + homeassistant/components/moehlenhoff_alpha2/climate.py + homeassistant/components/moehlenhoff_alpha2/const.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/cover.py @@ -716,6 +762,7 @@ omit = homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py + homeassistant/components/nanoleaf/device_trigger.py homeassistant/components/nanoleaf/diagnostics.py homeassistant/components/nanoleaf/entity.py homeassistant/components/nanoleaf/light.py @@ -730,9 +777,11 @@ omit = homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py + homeassistant/components/netgear/button.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear/router.py homeassistant/components/netgear/sensor.py + homeassistant/components/netgear/switch.py homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py @@ -759,6 +808,7 @@ omit = homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py + homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py homeassistant/components/obihai/* @@ -820,6 +870,8 @@ omit = homeassistant/components/overkiz/__init__.py homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/button.py + homeassistant/components/overkiz/climate.py + homeassistant/components/overkiz/climate_entities/* homeassistant/components/overkiz/cover.py homeassistant/components/overkiz/cover_entities/* homeassistant/components/overkiz/coordinator.py @@ -832,6 +884,7 @@ omit = homeassistant/components/overkiz/scene.py homeassistant/components/overkiz/select.py homeassistant/components/overkiz/sensor.py + homeassistant/components/overkiz/siren.py homeassistant/components/overkiz/switch.py homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py @@ -845,6 +898,7 @@ omit = homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py + homeassistant/components/philips_js/diagnostics.py homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py @@ -867,9 +921,13 @@ omit = homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py homeassistant/components/plex/media_player.py + homeassistant/components/plex/view.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py - homeassistant/components/point/* + homeassistant/components/point/__init__.py + homeassistant/components/point/alarm_control_panel.py + homeassistant/components/point/binary_sensor.py + homeassistant/components/point/sensor.py homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py @@ -892,8 +950,15 @@ omit = homeassistant/components/qrcode/image_processing.py homeassistant/components/quantum_gateway/device_tracker.py homeassistant/components/qvr_pro/* - homeassistant/components/rachio/* + homeassistant/components/rachio/__init__.py + homeassistant/components/rachio/binary_sensor.py + homeassistant/components/rachio/device.py + homeassistant/components/rachio/entity.py + homeassistant/components/rachio/switch.py + homeassistant/components/rachio/webhooks.py homeassistant/components/radarr/sensor.py + homeassistant/components/radio_browser/__init__.py + homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/climate.py homeassistant/components/rainbird/* homeassistant/components/raincloud/* @@ -948,7 +1013,6 @@ omit = homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* - homeassistant/components/scrape/sensor.py homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py @@ -973,6 +1037,9 @@ omit = homeassistant/components/senseme/switch.py homeassistant/components/sensibo/__init__.py homeassistant/components/sensibo/climate.py + homeassistant/components/sensibo/coordinator.py + homeassistant/components/sensibo/diagnostics.py + homeassistant/components/sensibo/number.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py @@ -985,6 +1052,7 @@ omit = homeassistant/components/shelly/climate.py homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py + homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py homeassistant/components/sht31/sensor.py @@ -1021,7 +1089,11 @@ omit = homeassistant/components/smarthab/__init__.py homeassistant/components/smarthab/cover.py homeassistant/components/smarthab/light.py - homeassistant/components/sms/* + homeassistant/components/sms/__init__.py + homeassistant/components/sms/const.py + homeassistant/components/sms/gateway.py + homeassistant/components/sms/notify.py + homeassistant/components/sms/sensor.py homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* homeassistant/components/snmp/* @@ -1030,7 +1102,8 @@ omit = homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py - homeassistant/components/solarlog/* + homeassistant/components/solarlog/__init__.py + homeassistant/components/solarlog/sensor.py homeassistant/components/solax/__init__.py homeassistant/components/solax/sensor.py homeassistant/components/soma/__init__.py @@ -1052,21 +1125,34 @@ omit = homeassistant/components/sonos/favorites.py homeassistant/components/sonos/helpers.py homeassistant/components/sonos/household_coordinator.py + homeassistant/components/sonos/media.py homeassistant/components/sonos/media_browser.py homeassistant/components/sonos/media_player.py homeassistant/components/sonos/speaker.py homeassistant/components/sonos/switch.py homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* - homeassistant/components/spider/* + homeassistant/components/spider/__init__.py + homeassistant/components/spider/climate.py + homeassistant/components/spider/sensor.py + homeassistant/components/spider/switch.py homeassistant/components/splunk/* homeassistant/components/spotify/__init__.py + homeassistant/components/spotify/browse_media.py homeassistant/components/spotify/media_player.py homeassistant/components/spotify/system_health.py + homeassistant/components/spotify/util.py homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/starline/* + homeassistant/components/starline/__init__.py + homeassistant/components/starline/account.py + homeassistant/components/starline/binary_sensor.py + homeassistant/components/starline/device_tracker.py + homeassistant/components/starline/entity.py + homeassistant/components/starline/lock.py + homeassistant/components/starline/sensor.py + homeassistant/components/starline/switch.py homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* @@ -1116,7 +1202,12 @@ omit = homeassistant/components/system_bridge/coordinator.py homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py - homeassistant/components/tado/* + homeassistant/components/tado/__init__.py + homeassistant/components/tado/binary_sensor.py + homeassistant/components/tado/climate.py + homeassistant/components/tado/device_tracker.py + homeassistant/components/tado/sensor.py + homeassistant/components/tado/water_heater.py homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/* homeassistant/components/tapsaff/binary_sensor.py @@ -1160,6 +1251,7 @@ omit = homeassistant/components/tolo/climate.py homeassistant/components/tolo/fan.py homeassistant/components/tolo/light.py + homeassistant/components/tolo/number.py homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py homeassistant/components/tomato/device_tracker.py @@ -1189,7 +1281,7 @@ omit = homeassistant/components/tractive/switch.py homeassistant/components/tradfri/__init__.py homeassistant/components/tradfri/base_class.py - homeassistant/components/tradfri/config_flow.py + homeassistant/components/tradfri/coordinator.py homeassistant/components/tradfri/cover.py homeassistant/components/tradfri/fan.py homeassistant/components/tradfri/light.py @@ -1237,7 +1329,9 @@ omit = homeassistant/components/upcloud/__init__.py homeassistant/components/upcloud/binary_sensor.py homeassistant/components/upcloud/switch.py - homeassistant/components/upnp/* + homeassistant/components/upnp/__init__.py + homeassistant/components/upnp/device.py + homeassistant/components/upnp/sensor.py homeassistant/components/upc_connect/* homeassistant/components/uscis/sensor.py homeassistant/components/vallox/__init__.py @@ -1251,6 +1345,7 @@ omit = homeassistant/components/velbus/climate.py homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py + homeassistant/components/velbus/diagnostics.py homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py @@ -1303,10 +1398,10 @@ omit = homeassistant/components/watson_tts/tts.py homeassistant/components/watttime/__init__.py homeassistant/components/watttime/sensor.py - homeassistant/components/waze_travel_time/__init__.py - homeassistant/components/waze_travel_time/helpers.py - homeassistant/components/waze_travel_time/sensor.py - homeassistant/components/wiffi/* + homeassistant/components/wiffi/__init__.py + homeassistant/components/wiffi/binary_sensor.py + homeassistant/components/wiffi/sensor.py + homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py homeassistant/components/wolflink/sensor.py @@ -1357,6 +1452,7 @@ omit = homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/const.py homeassistant/components/yale_smart_alarm/coordinator.py + homeassistant/components/yale_smart_alarm/diagnostics.py homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/lock.py homeassistant/components/yamaha_musiccast/__init__.py @@ -1396,6 +1492,16 @@ omit = homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/sensor.py + homeassistant/components/zwave_me/__init__.py + homeassistant/components/zwave_me/binary_sensor.py + homeassistant/components/zwave_me/button.py + homeassistant/components/zwave_me/climate.py + homeassistant/components/zwave_me/helpers.py + homeassistant/components/zwave_me/light.py + homeassistant/components/zwave_me/lock.py + homeassistant/components/zwave_me/number.py + homeassistant/components/zwave_me/sensor.py + homeassistant/components/zwave_me/switch.py [report] # Regexes for lines to exclude from consideration diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ac4c8453327a3..040f4a128eed0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -75,6 +75,17 @@ body: attributes: value: | # Details + - type: textarea + attributes: + label: Diagnostics information + description: >- + Many integrations provide the ability to download diagnostic data + on the device page (and on the integration dashboard). + + **It would really help if you could download the diagnostics data for the device you are having issues with, + and drag-and-drop that file into the textbox below.** + + It generally allows pinpointing defects and thus resolving issues faster. - type: textarea attributes: label: Example YAML snippet diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 74016d4492cd2..2ca9b754a3a5f 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -57,6 +57,7 @@ jobs: uses: home-assistant/actions/helpers/codenotary@master with: source: file://${{ github.workspace }}/OFFICIAL_IMAGE + asset: OFFICIAL_IMAGE-${{ steps.version.outputs.version }} token: ${{ secrets.CAS_TOKEN }} build_python: @@ -69,7 +70,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -103,7 +104,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -112,9 +113,8 @@ jobs: shell: bash run: | python3 -m pip install packaging - python3 -m pip install . - python3 script/version_bump.py nightly - version="$(python setup.py -V)" + python3 -m pip install --use-deprecated=legacy-resolver . + version="$(python3 script/version_bump.py nightly)" - name: Write meta info file shell: bash @@ -122,20 +122,20 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.12.0 + uses: home-assistant/builder@2022.01.0 with: args: | $BUILD_ARGS \ @@ -187,20 +187,20 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.12.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2021.12.0 + uses: home-assistant/builder@2022.01.0 with: args: | $BUILD_ARGS \ @@ -243,22 +243,30 @@ jobs: channel: beta publish_container: - name: Publish meta container + name: Publish meta container for ${{ matrix.registry }} if: github.repository_owner == 'home-assistant' needs: ["init", "build_base"] runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + registry: + - "ghcr.io/home-assistant" + - "homeassistant" steps: - name: Checkout the repository uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.12.0 + if: matrix.registry == 'homeassistant' + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.12.0 + if: matrix.registry == 'ghcr.io/home-assistant' + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -273,38 +281,37 @@ jobs: export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { - local docker_reg=${1} - local tag_l=${2} - local tag_r=${3} - - docker manifest create "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" - - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ + local tag_l=${1} + local tag_r=${2} + + docker manifest create "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" + + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ --os linux --arch amd64 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ --os linux --arch 386 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v6 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v7 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" \ --os linux --arch arm64 --variant=v8 - docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}" + docker manifest push --purge "${{ matrix.registry }}/home-assistant:${tag_l}" } function validate_image() { @@ -315,36 +322,34 @@ jobs: fi } - for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do - docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - - validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - - # Create version tag - create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" - - # Create general tags - if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then - create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}" - elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" - else - create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" - - # Create series version tag (e.g. 2021.6) - v="${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}" - fi - done + docker pull "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + validate_image "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + # Create version tag + create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" + + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + create_manifest"dev" "${{ needs.init.outputs.version }}" + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" + else + create_manifest "stable" "${{ needs.init.outputs.version }}" + create_manifest "latest" "${{ needs.init.outputs.version }}" + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" + + # Create series version tag (e.g. 2021.6) + v="${{ needs.init.outputs.version }}" + create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" + fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e87fb71e2be2..b1ff5dca8d1d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,10 +8,21 @@ on: - rc - master pull_request: ~ + workflow_dispatch: + inputs: + full: + description: 'Full run (regardless of changes)' + default: false + type: boolean + lint-only: + description: 'Skip pytest' + default: false + type: boolean env: - CACHE_VERSION: 5 - PIP_CACHE_VERSION: 1 + CACHE_VERSION: 9 + PIP_CACHE_VERSION: 3 + HA_SHORT_VERSION: 2022.3 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache @@ -107,7 +118,8 @@ jobs: if [[ "${{ github.ref }}" == "refs/heads/dev" ]] \ || [[ "${{ github.ref }}" == "refs/heads/master" ]] \ || [[ "${{ github.ref }}" == "refs/heads/rc" ]] \ - || [[ "${{ steps.core.outputs.any }}" == "true" ]]; + || [[ "${{ steps.core.outputs.any }}" == "true" ]] \ + || [[ "${{ github.event.inputs.full }}" == "true" ]]; then test_groups="[1, 2, 3, 4, 5, 6]" test_group_count=6 @@ -142,7 +154,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -155,8 +167,8 @@ jobs: - name: Generate partial pip restore key id: generate-pip-key run: >- - echo "::set-output name=key::base-pip-${{ env.PIP_CACHE_VERSION }}-$( - date -u '+%Y-%m-%dT%H:%M:%s')" + echo "::set-output name=key::base-pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -183,15 +195,15 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-pip-${{ env.PIP_CACHE_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip<20.3" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.1" setuptools wheel + pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt --use-deprecated=legacy-resolver - name: Generate partial pre-commit restore key id: generate-pre-commit-key run: >- @@ -222,7 +234,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -272,7 +284,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -323,7 +335,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -365,7 +377,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -499,7 +511,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -523,10 +535,10 @@ jobs: prepare-tests: name: Prepare tests for Python ${{ matrix.python-version }} runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 strategy: matrix: - python-version: [3.9] + python-version: ["3.9", "3.10"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} container: homeassistant/ci-azure:${{ matrix.python-version }} @@ -543,8 +555,8 @@ jobs: - name: Generate partial pip restore key id: generate-pip-key run: >- - echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-$( - date -u '+%Y-%m-%dT%H:%M:%s')" + echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -571,7 +583,7 @@ jobs: ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}- + ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -582,9 +594,9 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip<20.3" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements_all.txt - pip install --cache-dir=$PIP_CACHE -r requirements_test.txt + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.1" setuptools wheel + pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver + pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver pip install -e . pylint: @@ -706,7 +718,10 @@ jobs: pytest: runs-on: ubuntu-latest - if: needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob + if: | + (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') + && github.event.inputs.lint-only != 'true' + && (needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob) needs: - changes - gen-requirements-all @@ -720,7 +735,7 @@ jobs: fail-fast: false matrix: group: ${{ fromJson(needs.changes.outputs.test_groups) }} - python-version: [3.9] + python-version: ["3.9", "3.10"] name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) container: homeassistant/ci-azure:${{ matrix.python-version }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index e9badfc0479fe..1e637b02b1a4a 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v2.3.2 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6c9a7759c3d50..a0d6396ec3012 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -44,7 +44,7 @@ jobs: echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true" echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true" # GRPC on armv7 needs -lexecinfo (issue #56669) since home assistant installs - # execinfo-dev when building wheels. The setup.py does not have an option for + # execinfo-dev when building wheels. The setuptools build setup does not have an option for # adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0) echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo" ) > .env_file @@ -154,6 +154,7 @@ jobs: sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} + sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" ${requirement_file} done - name: Build wheels diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87c7d9e9102c7..7cdd28cd6e97f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black args: @@ -17,7 +17,7 @@ repos: 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 + - --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 - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] @@ -78,7 +78,7 @@ repos: - id: python-typing-update stages: [manual] args: - - --py38-plus + - --py39-plus - --force - --keep-updates files: ^(homeassistant|tests|script)/.+\.py$ @@ -114,11 +114,18 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|\.strict-typing|homeassistant/.+/services\.yaml|script/hassfest/.+\.py)$ + files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py)$ - id: hassfest-metadata name: hassfest-metadata entry: script/run-in-env.sh python3 -m script.hassfest -p metadata pass_filenames: false language: script types: [text] - files: ^(script/hassfest/.+\.py|homeassistant/const\.py$|setup\.cfg)$ + files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|setup\.cfg)$ + - id: hassfest-mypy-config + name: hassfest-mypy-config + entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config + pass_filenames: false + language: script + types: [text] + files: ^(script/hassfest/mypy_config\.py|\.strict-typing|mypy\.ini)$ diff --git a/.readthedocs.yml b/.readthedocs.yml index e8344e0a655c7..1a91abd9a996e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,10 +1,14 @@ # .readthedocs.yml +version: 2 + build: - image: latest + os: ubuntu-20.04 + tools: + python: "3.9" python: - version: 3.8 - setup_py_install: true - -requirements_file: requirements_docs.txt + install: + - method: setuptools + path: . + - requirements: requirements_docs.txt diff --git a/.strict-typing b/.strict-typing index 38c9e6812b7af..621c6c315fca2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -20,7 +20,9 @@ homeassistant.helpers.entity_values homeassistant.helpers.reload homeassistant.helpers.script_variables homeassistant.helpers.translation +homeassistant.util.async_ homeassistant.util.color +homeassistant.util.decorator homeassistant.util.process homeassistant.util.unit_system @@ -57,6 +59,11 @@ homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.crownstone.* homeassistant.components.cpuspeed.* +homeassistant.components.deconz +homeassistant.components.deconz.config_flow +homeassistant.components.deconz.diagnostics +homeassistant.components.deconz.gateway +homeassistant.components.deconz.services homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* @@ -87,6 +94,14 @@ homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event +homeassistant.components.homekit_controller +homeassistant.components.homekit_controller.alarm_control_panel +homeassistant.components.homekit_controller.button +homeassistant.components.homekit_controller.const +homeassistant.components.homekit_controller.lock +homeassistant.components.homekit_controller.select +homeassistant.components.homekit_controller.storage +homeassistant.components.homekit_controller.utils homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* @@ -95,6 +110,7 @@ homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* +homeassistant.components.isy994.* homeassistant.components.iqvia.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* @@ -109,6 +125,7 @@ homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* homeassistant.components.media_source.* @@ -132,8 +149,10 @@ homeassistant.components.openuv.* homeassistant.components.overkiz.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* +homeassistant.components.powerwall.* homeassistant.components.proximity.* homeassistant.components.pvoutput.* +homeassistant.components.pure_energie.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* @@ -144,6 +163,7 @@ homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* +homeassistant.components.roku.* homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* homeassistant.components.samsungtv.* @@ -154,8 +174,8 @@ homeassistant.components.senseme.* homeassistant.components.shelly.* homeassistant.components.simplisafe.* homeassistant.components.slack.* +homeassistant.components.sleepiq.* homeassistant.components.smhi.* -homeassistant.components.sonos.media_player homeassistant.components.ssdp.* homeassistant.components.stookalert.* homeassistant.components.statistics.* @@ -196,6 +216,7 @@ homeassistant.components.webostv.* homeassistant.components.websocket_api.* homeassistant.components.wemo.* homeassistant.components.whois.* +homeassistant.components.wiz.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3fecfd8ba4801..d71571d2594db 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -88,7 +88,7 @@ { "label": "Install all Requirements", "type": "shell", - "command": "pip3 install -r requirements_all.txt", + "command": "pip3 install --use-deprecated=legacy-resolver -r requirements_all.txt", "group": { "kind": "build", "isDefault": true @@ -102,7 +102,7 @@ { "label": "Install all Test Requirements", "type": "shell", - "command": "pip3 install -r requirements_test_all.txt", + "command": "pip3 install --use-deprecated=legacy-resolver -r requirements_test_all.txt", "group": { "kind": "build", "isDefault": true diff --git a/CODEOWNERS b/CODEOWNERS index 88669e8b5c719..283bd6442b1a8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,7 +4,7 @@ # https://github.com/blog/2392-introducing-code-owners # Home Assistant Core -setup.py @home-assistant/core +setup.cfg @home-assistant/core homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core @@ -43,8 +43,6 @@ homeassistant/components/airtouch4/* @LonePurpleWolf tests/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya tests/components/airvisual/* @bachya -homeassistant/components/alarmdecoder/* @ajschmidt8 -tests/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy tests/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob @@ -216,6 +214,8 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/dlna_dmr/* @StevenLooman @chishm tests/components/dlna_dmr/* @StevenLooman @chishm +homeassistant/components/dlna_dms/* @chishm +tests/components/dlna_dms/* @chishm homeassistant/components/dnsip/* @gjohansson-ST tests/components/dnsip/* @gjohansson-ST homeassistant/components/doorbird/* @oblogic7 @bdraco @flacjacket @@ -288,6 +288,8 @@ homeassistant/components/fireservicerota/* @cyberjunky tests/components/fireservicerota/* @cyberjunky homeassistant/components/firmata/* @DaAwesomeP tests/components/firmata/* @DaAwesomeP +homeassistant/components/fivem/* @Sander0542 +tests/components/fivem/* @Sander0542 homeassistant/components/fixer/* @fabaff homeassistant/components/fjaraskupan/* @elupus tests/components/fjaraskupan/* @elupus @@ -354,6 +356,8 @@ tests/components/goodwe/* @mletenay @starkillerOG homeassistant/components/google_assistant/* @home-assistant/cloud tests/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton +homeassistant/components/google_travel_time/* @eifinger +tests/components/google_travel_time/* @eifinger homeassistant/components/gpsd/* @fabaff homeassistant/components/gree/* @cmroche tests/components/gree/* @cmroche @@ -468,6 +472,8 @@ tests/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 tests/components/islamic_prayer_times/* @engrbm87 +homeassistant/components/iss/* @DurgNomis-drol +tests/components/iss/* @DurgNomis-drol homeassistant/components/isy994/* @bdraco @shbatm tests/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig @@ -571,6 +577,8 @@ homeassistant/components/modem_callerid/* @tkdrob tests/components/modem_callerid/* @tkdrob homeassistant/components/modern_forms/* @wonderslug tests/components/modern_forms/* @wonderslug +homeassistant/components/moehlenhoff_alpha2/* @j-a-n +tests/components/moehlenhoff_alpha2/* @j-a-n homeassistant/components/monoprice/* @etsinko @OnFreund tests/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff @@ -711,8 +719,8 @@ homeassistant/components/plaato/* @JohNan tests/components/plaato/* @JohNan homeassistant/components/plex/* @jjlawren tests/components/plex/* @jjlawren -homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra -tests/components/plugwise/* @CoMPaTech @bouwew @brefra +homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra @frenck +tests/components/plugwise/* @CoMPaTech @bouwew @brefra @frenck homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa tests/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike @@ -732,6 +740,8 @@ tests/components/prosegur/* @dgomes homeassistant/components/proxmoxve/* @jhollowe @Corbeno homeassistant/components/ps4/* @ktnrg45 tests/components/ps4/* @ktnrg45 +homeassistant/components/pure_energie/* @klaasnicolaas +tests/components/pure_energie/* @klaasnicolaas homeassistant/components/push/* @dgomes tests/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff @frenck @@ -747,6 +757,8 @@ homeassistant/components/qwikswitch/* @kellerza tests/components/qwikswitch/* @kellerza homeassistant/components/rachio/* @bdraco tests/components/rachio/* @bdraco +homeassistant/components/radio_browser/* @frenck +tests/components/radio_browser/* @frenck homeassistant/components/radiotherm/* @vinnyfuria homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator @@ -796,12 +808,13 @@ tests/components/ruckus_unleashed/* @gabe565 homeassistant/components/safe_mode/* @home-assistant/core tests/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl -homeassistant/components/samsungtv/* @escoand @chemelli74 -tests/components/samsungtv/* @escoand @chemelli74 +homeassistant/components/samsungtv/* @escoand @chemelli74 @epenet +tests/components/samsungtv/* @escoand @chemelli74 @epenet homeassistant/components/scene/* @home-assistant/core tests/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff +tests/components/scrape/* @fabaff homeassistant/components/screenlogic/* @dieselrabbit @bdraco tests/components/screenlogic/* @dieselrabbit @bdraco homeassistant/components/script/* @home-assistant/core @@ -843,6 +856,8 @@ homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya tests/components/slack/* @bachya +homeassistant/components/sleepiq/* @mfugate1 @kbickar +tests/components/sleepiq/* @mfugate1 @kbickar homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza @rklomp tests/components/sma/* @kellerza @rklomp @@ -1031,8 +1046,8 @@ tests/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 tests/components/vizio/* @raman325 -homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare -tests/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare +homeassistant/components/vlc_telnet/* @rodripf @MartinHjelmare +tests/components/vlc_telnet/* @rodripf @MartinHjelmare homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund tests/components/volumio/* @OnFreund @@ -1045,6 +1060,8 @@ homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/watttime/* @bachya tests/components/watttime/* @bachya +homeassistant/components/waze_travel_time/* @eifinger +tests/components/waze_travel_time/* @eifinger homeassistant/components/weather/* @fabaff tests/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid @thecode @@ -1064,6 +1081,8 @@ tests/components/wilight/* @leofig-rj homeassistant/components/wirelesstag/* @sergeymaysak homeassistant/components/withings/* @vangorra tests/components/withings/* @vangorra +homeassistant/components/wiz/* @sbidy +tests/components/wiz/* @sbidy homeassistant/components/wled/* @frenck tests/components/wled/* @frenck homeassistant/components/wolflink/* @adamkrol93 @@ -1087,8 +1106,8 @@ homeassistant/components/yamaha_musiccast/* @vigonotion @micha91 tests/components/yamaha_musiccast/* @vigonotion @micha91 homeassistant/components/yandex_transport/* @rishatik92 @devbis tests/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @zewelor @shenxn @starkillerOG -tests/components/yeelight/* @zewelor @shenxn @starkillerOG +homeassistant/components/yeelight/* @zewelor @shenxn @starkillerOG @alexyao2015 +tests/components/yeelight/* @zewelor @shenxn @starkillerOG @alexyao2015 homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yi/* @bachya homeassistant/components/youless/* @gjong @@ -1108,6 +1127,8 @@ homeassistant/components/zwave/* @home-assistant/z-wave tests/components/zwave/* @home-assistant/z-wave homeassistant/components/zwave_js/* @home-assistant/z-wave tests/components/zwave_js/* @home-assistant/z-wave +homeassistant/components/zwave_me/* @lawfulchaos @Z-Wave-Me +tests/components/zwave_me/* @lawfulchaos @Z-Wave-Me # Individual files homeassistant/components/demo/weather @fabaff diff --git a/Dockerfile b/Dockerfile index a4d5ce3045dc1..7193d706b8995 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,17 +12,18 @@ COPY requirements.txt homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements.txt + -r homeassistant/requirements.txt --use-deprecated=legacy-resolver COPY requirements_all.txt homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt + sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" homeassistant/requirements_all.txt \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -e ./homeassistant \ + -e ./homeassistant --use-deprecated=legacy-resolver \ && python3 -m compileall homeassistant/homeassistant # Fix Bug with Alpine 3.14 and sqlite 3.35 diff --git a/Dockerfile.dev b/Dockerfile.dev index b908bf01a32ce..39ce36074ad4b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -33,9 +33,9 @@ WORKDIR /workspaces # Install Python dependencies from requirements COPY requirements.txt ./ COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN pip3 install -r requirements.txt +RUN pip3 install -r requirements.txt --use-deprecated=legacy-resolver COPY requirements_test.txt requirements_test_pre_commit.txt ./ -RUN pip3 install -r requirements_test.txt +RUN pip3 install -r requirements_test.txt --use-deprecated=legacy-resolver RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/ # Set the default shell to bash instead of sh diff --git a/build.yaml b/build.yaml index 1d0e18c79ea88..3ced7b8d742b1 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:2021.09.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2021.09.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2021.09.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2021.09.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2021.09.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.02.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.02.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.02.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.02.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.02.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/async_timeout_backcompat.py b/homeassistant/async_timeout_backcompat.py deleted file mode 100644 index 212beddfae326..0000000000000 --- a/homeassistant/async_timeout_backcompat.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Provide backwards compat for async_timeout.""" -from __future__ import annotations - -import asyncio -from typing import Any - -import async_timeout - -from .helpers.frame import report - - -def timeout( - delay: float | None, loop: asyncio.AbstractEventLoop | None = None -) -> async_timeout.Timeout: - """Backwards compatible timeout context manager that warns with loop usage.""" - if loop is None: - loop = asyncio.get_running_loop() - else: - report( - "called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.3", - error_if_core=False, - ) - if delay is not None: - deadline: float | None = loop.time() + delay - else: - deadline = None - return async_timeout.Timeout(deadline, loop) - - -def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None: - """Backwards compatible current_task.""" - report( - "called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.3; use asyncio.current_task instead", - error_if_core=False, - ) - return asyncio.current_task() - - -def enable() -> None: - """Enable backwards compat transitions.""" - async_timeout.timeout = timeout - async_timeout.current_task = current_task # type: ignore[attr-defined] diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index b98dc07deb6ed..b3f57656dd70f 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -354,7 +354,7 @@ async def async_remove_credentials(self, credentials: models.Credentials) -> Non 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 + await provider.async_will_remove_credentials(credentials) # type: ignore[attr-defined] await self._store.async_remove_credentials(credentials) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 60f790fa4907b..61c36da6e9032 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry -MULTI_FACTOR_AUTH_MODULES = Registry() +MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry() MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema( { @@ -55,7 +55,7 @@ def id(self) -> str: @property def type(self) -> str: """Return type of the module.""" - return self.config[CONF_TYPE] # type: ignore + return self.config[CONF_TYPE] # type: ignore[no-any-return] @property def name(self) -> str: @@ -129,7 +129,7 @@ async def auth_mfa_module_from_config( hass: HomeAssistant, config: dict[str, Any] ) -> MultiFactorAuthModule: """Initialize an auth module from a config.""" - module_name = config[CONF_TYPE] + module_name: str = config[CONF_TYPE] module = await _load_mfa_module(hass, module_name) try: @@ -142,7 +142,7 @@ async def auth_mfa_module_from_config( ) raise - return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore + return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType: diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 71fdbadbb9007..37b35a5087d9b 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -251,7 +251,7 @@ async def async_notify_user(self, user_id: str, code: str) -> None: await self.async_notify( code, - notify_setting.notify_service, # type: ignore + notify_setting.notify_service, # type: ignore[arg-type] notify_setting.target, ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index c979ba05b5aef..bb5fc47469fbf 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -107,7 +107,7 @@ def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: ota_secret: str = secret or pyotp.random_base32() - self._users[user_id] = ota_secret # type: ignore + self._users[user_id] = ota_secret # type: ignore[index] return ota_secret async def async_setup_flow(self, user_id: str) -> SetupFlow: @@ -136,7 +136,7 @@ async def async_depose_user(self, user_id: str) -> None: if self._users is None: await self._async_load() - if self._users.pop(user_id, None): # type: ignore + if self._users.pop(user_id, None): # type: ignore[union-attr] await self._async_save() async def async_is_user_setup(self, user_id: str) -> bool: @@ -144,7 +144,7 @@ async def async_is_user_setup(self, user_id: str) -> bool: if self._users is None: await self._async_load() - return user_id in self._users # type: ignore + return user_id in self._users # type: ignore[operator] async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: """Return True if validation passed.""" @@ -161,7 +161,7 @@ def _validate_2fa(self, user_id: str, code: str) -> bool: """Validate two factor authentication code.""" import pyotp # pylint: disable=import-outside-toplevel - if (ota_secret := self._users.get(user_id)) is None: # type: ignore + if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr] # even we cannot find user, we still do verify # to make timing the same as if user was found. pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1) @@ -182,8 +182,8 @@ def __init__( self._auth_module: TotpAuthModule = auth_module self._user = user self._ota_secret: str = "" - self._url = None # type Optional[str] - self._image = None # type Optional[str] + self._url: str | None = None + self._image: str | None = None async def async_step_init( self, user_input: dict[str, str] | None = None @@ -218,7 +218,7 @@ async def async_step_init( self._url, self._image, ) = await hass.async_add_executor_job( - _generate_secret_and_qr_code, # type: ignore + _generate_secret_and_qr_code, str(self._user.name), ) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index b209f6b2f05a8..63389059051ae 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" -AUTH_PROVIDERS = Registry() +AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry() AUTH_PROVIDER_SCHEMA = vol.Schema( { @@ -62,7 +62,7 @@ def id(self) -> str | None: @property def type(self) -> str: """Return type of the provider.""" - return self.config[CONF_TYPE] # type: ignore + return self.config[CONF_TYPE] # type: ignore[no-any-return] @property def name(self) -> str: @@ -136,7 +136,7 @@ async def auth_provider_from_config( hass: HomeAssistant, store: AuthStore, config: dict[str, Any] ) -> AuthProvider: """Initialize an auth provider from a config.""" - provider_name = config[CONF_TYPE] + provider_name: str = config[CONF_TYPE] module = await load_auth_provider_module(hass, provider_name) try: @@ -149,7 +149,7 @@ async def auth_provider_from_config( ) raise - return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore + return AUTH_PROVIDERS[provider_name](hass, store, config) async def load_auth_provider_module( @@ -250,7 +250,7 @@ async def async_step_mfa( auth_module, "async_initialize_login_mfa_step" ): try: - await auth_module.async_initialize_login_mfa_step( # type: ignore + await auth_module.async_initialize_login_mfa_step( # type: ignore[attr-defined] self.user.id ) except HomeAssistantError: diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 4fb12e3e76420..a4797dd8c1054 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -120,7 +120,7 @@ async def async_load(self) -> None: @property def users(self) -> list[dict[str, str]]: """Return users.""" - return self._data["users"] # type: ignore + return self._data["users"] # type: ignore[index,no-any-return] def validate_login(self, username: str, password: str) -> None: """Validate a username and password. diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 9358fe7311093..29e31ae4a88f2 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -8,7 +8,7 @@ def enable() -> None: """Enable the detection of blocking calls in the event loop.""" # Prevent urllib3 and requests doing I/O in event loop - HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore + HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore[assignment] # Prevent sleeping in event loop. Non-strict since 2022.02 time.sleep = protect_loop(time.sleep, strict=False) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 49282c70cb087..986171cbee79e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -3,7 +3,7 @@ import asyncio import contextlib -from datetime import datetime +from datetime import datetime, timedelta import logging import logging.handlers import os @@ -59,7 +59,7 @@ MAX_LOAD_CONCURRENTLY = 6 DEBUGGER_INTEGRATIONS = {"debugpy"} -CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") +CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} LOGGING_INTEGRATIONS = { # Set log levels "logger", @@ -69,7 +69,14 @@ # To record data "recorder", } +DISCOVERY_INTEGRATIONS = ("dhcp", "ssdp", "usb", "zeroconf") STAGE_1_INTEGRATIONS = { + # We need to make sure discovery integrations + # update their deps before stage 2 integrations + # load them inadvertently before their deps have + # been updated which leads to using an old version + # of the dep, or worse (import errors). + *DISCOVERY_INTEGRATIONS, # To make sure we forward data to other instances "mqtt_eventstream", # To provide account link implementations @@ -151,8 +158,11 @@ async def async_setup_hass( safe_mode = True old_config = hass.config + old_logging = hass.data.get(DATA_LOGGING) hass = core.HomeAssistant() + if old_logging: + hass.data[DATA_LOGGING] = old_logging hass.config.skip_pip = old_config.skip_pip hass.config.internal_url = old_config.internal_url hass.config.external_url = old_config.external_url @@ -314,7 +324,7 @@ def async_enable_logging( logging.getLogger("aiohttp.access").setLevel(logging.WARNING) sys.excepthook = lambda *args: logging.getLogger(None).exception( - "Uncaught exception", exc_info=args # type: ignore + "Uncaught exception", exc_info=args # type: ignore[arg-type] ) threading.excepthook = lambda args: logging.getLogger(None).exception( "Uncaught thread exception", @@ -450,7 +460,7 @@ async def _async_set_up_integrations( ) -> None: """Set up all the integrations.""" hass.data[DATA_SETUP_STARTED] = {} - setup_time = hass.data[DATA_SETUP_TIME] = {} + setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {}) watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) @@ -459,9 +469,9 @@ async def _async_set_up_integrations( # Resolve all dependencies so we know all integrations # that will have to be loaded and start rightaway integration_cache: dict[str, loader.Integration] = {} - to_resolve = domains_to_setup + to_resolve: set[str] = domains_to_setup while to_resolve: - old_to_resolve = to_resolve + old_to_resolve: set[str] = to_resolve to_resolve = set() integrations_to_process = [ @@ -508,11 +518,11 @@ async def _async_set_up_integrations( await async_setup_multi_components(hass, debuggers, config) # calculate what components to setup in what stage - stage_1_domains = set() + stage_1_domains: set[str] = set() # Find all dependencies of any dependency of any stage 1 integration that # we plan on loading and promote them to stage 1 - deps_promotion = STAGE_1_INTEGRATIONS + deps_promotion: set[str] = STAGE_1_INTEGRATIONS while deps_promotion: old_deps_promotion = deps_promotion deps_promotion = set() @@ -577,7 +587,7 @@ async def _async_set_up_integrations( { integration: timedelta.total_seconds() for integration, timedelta in sorted( - setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore + setup_time.items(), key=lambda item: item[1].total_seconds() ) }, ) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 9885ccb54ef2e..1eb6f859d0c80 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -88,6 +88,8 @@ def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Get a camera image.""" + if not self.capture(): + return None self.refresh_image() if self._response: diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index c9353c31bab32..07fcfe6cb74b9 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -8,5 +8,6 @@ "homekit": { "models": ["Abode", "Iota"] }, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["abodepy", "lomond"] } diff --git a/homeassistant/components/abode/translations/el.json b/homeassistant/components/abode/translations/el.json index caa58ca58e290..b11c3cb6dbe74 100644 --- a/homeassistant/components/abode/translations/el.json +++ b/homeassistant/components/abode/translations/el.json @@ -1,11 +1,33 @@ { "config": { + "abort": { + "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." + }, "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "invalid_mfa_code": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 MFA" }, "step": { + "mfa": { + "data": { + "mfa_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 MFA (6 \u03c8\u03b7\u03c6\u03af\u03b1)" + }, + "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc MFA \u03b3\u03b9\u03b1 \u03c4\u03bf Abode" + }, "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, + "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Abode" + }, + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Abode" } } diff --git a/homeassistant/components/abode/translations/pt-BR.json b/homeassistant/components/abode/translations/pt-BR.json index 4c61f5d243d9b..27f72830f87ee 100644 --- a/homeassistant/components/abode/translations/pt-BR.json +++ b/homeassistant/components/abode/translations/pt-BR.json @@ -1,14 +1,34 @@ { "config": { "abort": { - "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Abode \u00e9 permitida." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Digite seu c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Email" + }, + "title": "Preencha as informa\u00e7\u00f5es de login da Abode" + }, "user": { "data": { "password": "Senha", - "username": "Endere\u00e7o de e-mail" - } + "username": "Email" + }, + "title": "Preencha suas informa\u00e7\u00f5es de login Abode" } } } diff --git a/homeassistant/components/abode/translations/sk.json b/homeassistant/components/abode/translations/sk.json new file mode 100644 index 0000000000000..2230fa979b459 --- /dev/null +++ b/homeassistant/components/abode/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "Email" + } + }, + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/uk.json b/homeassistant/components/abode/translations/uk.json index 7ad57a0ec68a6..4ff498f98d625 100644 --- a/homeassistant/components/abode/translations/uk.json +++ b/homeassistant/components/abode/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json index 6725df4445162..9a7136223b962 100644 --- a/homeassistant/components/abode/translations/zh-Hant.json +++ b/homeassistant/components/abode/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index fd391a81bad24..cbb74585778d4 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["accuweather"] } diff --git a/homeassistant/components/accuweather/translations/el.json b/homeassistant/components/accuweather/translations/el.json index 20c3b56bd60c7..4f7a23e1d6f6c 100644 --- a/homeassistant/components/accuweather/translations/el.json +++ b/homeassistant/components/accuweather/translations/el.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "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", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "requests_exceeded": "\u0388\u03c7\u03b5\u03b9 \u03be\u03b5\u03c0\u03b5\u03c1\u03b1\u03c3\u03c4\u03b5\u03af \u03bf \u03b5\u03c0\u03b9\u03c4\u03c1\u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03c9\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf API \u03c4\u03bf\u03c5 Accuweather. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API." + }, "step": { "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, "description": "\u0391\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03c1\u03af\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b1\u03c4\u03b9\u03ac \u03b5\u03b4\u03ce: https://www.home-assistant.io/integrations/accuweather/\n\n\u039f\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03b7\u03c4\u03c1\u03ce\u03bf \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.\n\u0397 \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.", "title": "AccuWeather" } @@ -12,12 +26,15 @@ "user": { "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 API \u03c4\u03bf\u03c5 AccuWeather, \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 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\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.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 AccuWeather" } } }, "system_health": { "info": { + "can_reach_server": "\u03a0\u03c1\u03bf\u03c3\u03b5\u03b3\u03b3\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae AccuWeather", "remaining_requests": "\u03a5\u03c0\u03bf\u03bb\u03b5\u03b9\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1" } } diff --git a/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant/components/accuweather/translations/pt-BR.json index 75111f9892daf..469dd81ff9129 100644 --- a/homeassistant/components/accuweather/translations/pt-BR.json +++ b/homeassistant/components/accuweather/translations/pt-BR.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "requests_exceeded": "O n\u00famero permitido de solicita\u00e7\u00f5es para a API Accuweather foi excedido. Voc\u00ea precisa esperar ou alterar a chave de API." + }, "step": { "user": { "data": { - "api_key": "Chave API", + "api_key": "Chave da API", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "name": "Nome" }, + "description": "Se precisar de ajuda com a configura\u00e7\u00e3o, d\u00ea uma olhada aqui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlguns sensores n\u00e3o s\u00e3o ativados por padr\u00e3o. Voc\u00ea pode habilit\u00e1-los no registro da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\nA previs\u00e3o do tempo n\u00e3o est\u00e1 habilitada por padr\u00e3o. Voc\u00ea pode habilit\u00e1-lo nas op\u00e7\u00f5es de integra\u00e7\u00e3o.", "title": "AccuWeather" } } @@ -17,8 +27,15 @@ "data": { "forecast": "Previs\u00e3o do Tempo" }, - "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos." + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos.", + "title": "Op\u00e7\u00f5es do AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcance o servidor AccuWeather", + "remaining_requests": "Solicita\u00e7\u00f5es permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.el.json b/homeassistant/components/accuweather/translations/sensor.el.json new file mode 100644 index 0000000000000..2e90f28e92ae3 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.el.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u03a0\u03c4\u03ce\u03c3\u03b7", + "rising": "\u0391\u03c5\u03be\u03b1\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7", + "steady": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.pt-BR.json b/homeassistant/components/accuweather/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..1e9cca9b30b5f --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Queda", + "rising": "Eleva\u00e7\u00e3o", + "steady": "Est\u00e1vel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sk.json b/homeassistant/components/accuweather/translations/sk.json new file mode 100644 index 0000000000000..8e0bc629a1328 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index 7432d0df48435..cb02c7e0ad522 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index 11df415d4c963..fbf72991e92ac 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index 6313b177f4765..c47a283124656 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/acmeda", "requirements": ["aiopulse==0.4.3"], "codeowners": ["@atmurray"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiopulse"] } diff --git a/homeassistant/components/acmeda/translations/el.json b/homeassistant/components/acmeda/translations/el.json index 314fa9941676d..9ce98dbfca3d1 100644 --- a/homeassistant/components/acmeda/translations/el.json +++ b/homeassistant/components/acmeda/translations/el.json @@ -1,5 +1,8 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/acmeda/translations/es-419.json b/homeassistant/components/acmeda/translations/es-419.json new file mode 100644 index 0000000000000..fff5e8fa56591 --- /dev/null +++ b/homeassistant/components/acmeda/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Elija un concentrador para agregar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/pt-BR.json b/homeassistant/components/acmeda/translations/pt-BR.json new file mode 100644 index 0000000000000..33d9f11e95e3a --- /dev/null +++ b/homeassistant/components/acmeda/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "id": "ID do host" + }, + "title": "Escolha um hub para adicionar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 48cbc9b270c76..ea8e35547469f 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -146,8 +146,7 @@ def __init__(self, adax_data_handler, unique_id): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._adax_data_handler.set_target_temperature(temperature) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 70b0eff18e35f..73ab948ecb4b7 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@danielhiversen" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["adax", "adax_local"] } diff --git a/homeassistant/components/adax/translations/el.json b/homeassistant/components/adax/translations/el.json new file mode 100644 index 0000000000000..ead0eb7e4f181 --- /dev/null +++ b/homeassistant/components/adax/translations/el.json @@ -0,0 +1,38 @@ +{ + "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", + "heater_not_available": "\u039f \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 + \u03ba\u03b1\u03b9 OK \u03b3\u03b9\u03b1 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1.", + "heater_not_found": "\u039f \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b1 \u03c0\u03b9\u03bf \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03c4\u03bf\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae Home Assistant.", + "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" + }, + "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" + }, + "step": { + "cloud": { + "data": { + "account_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + }, + "local": { + "data": { + "wifi_pswd": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b1 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 + \u03ba\u03b1\u03b9 OK \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03bf\u03b8\u03cc\u03bd\u03b7 \u03b7 \u03ad\u03bd\u03b4\u03b5\u03b9\u03be\u03b7 \"Reset\" (\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac). \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\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 OK \u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03ac\u03c3\u03c4\u03c1\u03b1 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03b5\u03b9 \u03c4\u03bf \u03bc\u03c0\u03bb\u03b5 led \u03c0\u03c1\u03b9\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae. \u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b8\u03b5\u03c1\u03bc\u03ac\u03c3\u03c4\u03c1\u03b1\u03c2 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac." + }, + "user": { + "data": { + "account_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", + "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "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" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b8\u03b5\u03c1\u03bc\u03ac\u03c3\u03c4\u03c1\u03b5\u03c2 \u03bc\u03b5 bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/es-419.json b/homeassistant/components/adax/translations/es-419.json index e2a1fdbc9f322..85df150d6630b 100644 --- a/homeassistant/components/adax/translations/es-419.json +++ b/homeassistant/components/adax/translations/es-419.json @@ -1,8 +1,26 @@ { "config": { + "abort": { + "heater_not_available": "Calentador no disponible. Intente restablecer el calentador presionando + y OK durante algunos segundos.", + "heater_not_found": "No se encontr\u00f3 el calentador. Intente acercar el calentador a la computadora de Home Assistant." + }, "step": { + "cloud": { + "data": { + "account_id": "ID de cuenta", + "password": "Contrase\u00f1a" + } + }, + "local": { + "data": { + "wifi_pswd": "Contrase\u00f1a de Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "Reinicie el calentador presionando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mantenga presionado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." + }, "user": { "data": { + "account_id": "ID de cuenta", "connection_type": "Seleccione el tipo de conexi\u00f3n" }, "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores con bluetooth" diff --git a/homeassistant/components/adax/translations/lv.json b/homeassistant/components/adax/translations/lv.json new file mode 100644 index 0000000000000..3ae3e819b7edd --- /dev/null +++ b/homeassistant/components/adax/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "account_id": "Konta ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/pt-BR.json b/homeassistant/components/adax/translations/pt-BR.json new file mode 100644 index 0000000000000..09498932c202b --- /dev/null +++ b/homeassistant/components/adax/translations/pt-BR.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "heater_not_available": "Aquecedor n\u00e3o dispon\u00edvel. Tente reiniciar o aquecedor pressionando + e OK por alguns segundos.", + "heater_not_found": "Aquecedor n\u00e3o encontrado. Tente aproximar o aquecedor do computador do Home Assistant.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "cloud": { + "data": { + "account_id": "ID da conta", + "password": "Senha" + } + }, + "local": { + "data": { + "wifi_pswd": "Senha do Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "Reinicie o aquecedor pressionando + e OK at\u00e9 que o display mostre 'Reset'. Em seguida, pressione e segure o bot\u00e3o OK no aquecedor at\u00e9 que o led azul comece a piscar antes de pressionar Enviar. A configura\u00e7\u00e3o do aquecedor pode levar alguns minutos." + }, + "user": { + "data": { + "account_id": "ID da conta", + "connection_type": "Selecione o tipo de conex\u00e3o", + "host": "Nome do host", + "password": "Senha" + }, + "description": "Selecione o tipo de conex\u00e3o. Local requer aquecedores com bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/sk.json b/homeassistant/components/adax/translations/sk.json new file mode 100644 index 0000000000000..2c3ed1dd93049 --- /dev/null +++ b/homeassistant/components/adax/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 8c90efcd44c8f..1f2645e227c58 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -205,7 +205,7 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] }, manufacturer="AdGuard Team", name="AdGuard Home", diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 9dc72ad76d9f7..cf1210b0884e7 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": ["adguardhome==0.5.1"], "codeowners": ["@frenck"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["adguardhome"] } diff --git a/homeassistant/components/adguard/translations/el.json b/homeassistant/components/adguard/translations/el.json index e7a56e8f73620..37b242811babe 100644 --- a/homeassistant/components/adguard/translations/el.json +++ b/homeassistant/components/adguard/translations/el.json @@ -1,5 +1,9 @@ { "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", + "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." + }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, @@ -9,6 +13,14 @@ "title": "AdGuard Home \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Home Assistant" }, "user": { + "data": { + "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", + "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" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf AdGuard Home \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf." } } diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index 5d291f4cadb64..1fd8618806311 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -1,19 +1,25 @@ { "config": { "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, + "error": { + "cannot_connect": "Falha ao conectar" + }, "step": { "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?", - "title": "AdGuard Home via add-on Supervisor" + "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo add-on {addon}?", + "title": "AdGuard Home via add-on" }, "user": { "data": { + "host": "Nome do host", "password": "Senha", - "ssl": "O AdGuard Home usa um certificado SSL", + "port": "Porta", + "ssl": "Usar um certificado SSL", "username": "Usu\u00e1rio", - "verify_ssl": "O AdGuard Home usa um certificado apropriado" + "verify_ssl": "Verifique o certificado SSL" }, "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle." } diff --git a/homeassistant/components/adguard/translations/sk.json b/homeassistant/components/adguard/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/adguard/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 9e4f838440460..06e11f9ae8b24 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": ["pyads==3.2.2"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyads"] } diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 56c89fe734686..6f1d811c291d4 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -7,8 +7,11 @@ "@Bre77" ], "requirements": [ - "advantage_air==0.3.0" + "advantage_air==0.3.1" ], "quality_scale": "platinum", - "iot_class": "local_polling" -} + "iot_class": "local_polling", + "loggers": [ + "advantage_air" + ] +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/el.json b/homeassistant/components/advantage_air/translations/el.json index e4dc05db74a6c..b49125616cf50 100644 --- a/homeassistant/components/advantage_air/translations/el.json +++ b/homeassistant/components/advantage_air/translations/el.json @@ -1,7 +1,17 @@ { "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" + }, "step": { "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "port": "\u0398\u03cd\u03c1\u03b1" + }, "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf API \u03c4\u03bf\u03c5 \u03b5\u03c0\u03af\u03c4\u03bf\u03b9\u03c7\u03bf\u03c5 tablet Advantage Air.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7" } diff --git a/homeassistant/components/advantage_air/translations/pt-BR.json b/homeassistant/components/advantage_air/translations/pt-BR.json new file mode 100644 index 0000000000000..f2ec82e1a20d1 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "description": "Conecte-se \u00e0 API do seu tablet Advantage Air montado na parede.", + "title": "Conectar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/sk.json b/homeassistant/components/advantage_air/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index e69980ad1220a..087d5c3882044 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "requirements": ["AEMET-OpenData==0.2.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aemet_opendata"] } diff --git a/homeassistant/components/aemet/translations/el.json b/homeassistant/components/aemet/translations/el.json index 5e66d06c6cae8..757fd41be703f 100644 --- a/homeassistant/components/aemet/translations/el.json +++ b/homeassistant/components/aemet/translations/el.json @@ -1,8 +1,17 @@ { "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" + }, + "error": { + "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", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 AEMET OpenData. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://opendata.aemet.es/centrodedescargas/altaUsuario", diff --git a/homeassistant/components/aemet/translations/pt-BR.json b/homeassistant/components/aemet/translations/pt-BR.json new file mode 100644 index 0000000000000..6a0c8800b026d --- /dev/null +++ b/homeassistant/components/aemet/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome da integra\u00e7\u00e3o" + }, + "description": "Configure a integra\u00e7\u00e3o AEMET OpenData. Para gerar a chave API acesse https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Colete dados das esta\u00e7\u00f5es meteorol\u00f3gicas da AEMET" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/sk.json b/homeassistant/components/aemet/translations/sk.json new file mode 100644 index 0000000000000..3c287c2d9d28a --- /dev/null +++ b/homeassistant/components/aemet/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index 311674359c7ff..6b74c771b038f 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -7,4 +7,4 @@ ], "codeowners": [], "iot_class": "cloud_polling" -} \ No newline at end of file +} diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 82b844bbd01c1..62d138d212362 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -149,10 +149,10 @@ async def async_update(self, **kwargs: Any) -> None: status_to_ignore = {"delivered"} status_counts: dict[str, int] = {} - trackings = [] + parsed_trackings = [] not_delivered_count = 0 - for track in trackings: + for track in trackings["trackings"]: status = track["tag"].lower() name = ( track["tracking_number"] if track["title"] is None else track["title"] @@ -163,7 +163,7 @@ async def async_update(self, **kwargs: Any) -> None: else track["checkpoints"][-1] ) status_counts[status] = status_counts.get(status, 0) + 1 - trackings.append( + parsed_trackings.append( { "name": name, "tracking_number": track["tracking_number"], @@ -183,7 +183,7 @@ async def async_update(self, **kwargs: Any) -> None: self._attributes = { **status_counts, - ATTR_TRACKINGS: trackings, + ATTR_TRACKINGS: parsed_trackings, } self._state = not_delivered_count diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 474d1f08b80db..e82bbeaea1bed 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -5,13 +5,8 @@ from agent import AgentError from homeassistant.components.camera import SUPPORT_ON_OFF -from homeassistant.components.mjpeg.camera import ( - CONF_MJPEG_URL, - CONF_STILL_IMAGE_URL, - MjpegCamera, - filter_urllib3_logging, -) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers import entity_platform from homeassistant.helpers.entity import DeviceInfo @@ -70,16 +65,15 @@ class AgentCamera(MjpegCamera): def __init__(self, device): """Initialize as a subclass of MjpegCamera.""" - device_info = { - CONF_NAME: device.name, - CONF_MJPEG_URL: f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", - CONF_STILL_IMAGE_URL: f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", - } self.device = device self._removed = False self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" - super().__init__(device_info) + super().__init__( + name=device.name, + mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + ) self._attr_device_info = DeviceInfo( identifiers={(AGENT_DOMAIN, self.unique_id)}, manufacturer="Agent", diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 7d740bbe7310d..c7ac3e14022a0 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -5,5 +5,6 @@ "requirements": ["agent-py==0.0.23"], "config_flow": true, "codeowners": ["@ispysoftware"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["agent"] } diff --git a/homeassistant/components/agent_dvr/translations/el.json b/homeassistant/components/agent_dvr/translations/el.json new file mode 100644 index 0000000000000..b9990eb1c04aa --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/el.json @@ -0,0 +1,20 @@ +{ + "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": { + "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" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/pt-BR.json b/homeassistant/components/agent_dvr/translations/pt-BR.json index 0077ceddd4605..5dab2c95651ba 100644 --- a/homeassistant/components/agent_dvr/translations/pt-BR.json +++ b/homeassistant/components/agent_dvr/translations/pt-BR.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar" + }, "step": { "user": { "data": { + "host": "Nome do host", "port": "Porta" - } + }, + "title": "Configurar agente DVR" } } } diff --git a/homeassistant/components/agent_dvr/translations/sk.json b/homeassistant/components/agent_dvr/translations/sk.json new file mode 100644 index 0000000000000..ba2680ac75e21 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index 430e51c6e9e07..56dd205de6899 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -6,5 +6,6 @@ "requirements": ["airly==1.1.0"], "config_flow": true, "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["airly"] } diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json index 30677a8504195..adb944a425962 100644 --- a/homeassistant/components/airly/translations/el.json +++ b/homeassistant/components/airly/translations/el.json @@ -1,11 +1,28 @@ { "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" + }, "error": { - "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af" + "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af", + "wrong_location": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 Airly \u03c3\u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae \u03b1\u03c5\u03c4\u03ae." + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03ad\u03c1\u03b1 Airly. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.airly.eu/register", + "title": "Airly" + } } }, "system_health": { "info": { + "can_reach_server": "\u03a0\u03c1\u03bf\u03c3\u03ad\u03b3\u03b3\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Airly", "requests_per_day": "\u0395\u03c0\u03b9\u03c4\u03c1\u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b7\u03bc\u03ad\u03c1\u03b1", "requests_remaining": "\u03a5\u03c0\u03bf\u03bb\u03b5\u03b9\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1" } diff --git a/homeassistant/components/airly/translations/pt-BR.json b/homeassistant/components/airly/translations/pt-BR.json new file mode 100644 index 0000000000000..e1f2fe097a682 --- /dev/null +++ b/homeassistant/components/airly/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida", + "wrong_location": "N\u00e3o h\u00e1 esta\u00e7\u00f5es de medi\u00e7\u00e3o a\u00e9reas nesta \u00e1rea." + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Configure a integra\u00e7\u00e3o da qualidade do ar airly. Para gerar a chave de API v\u00e1 para https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Alcance o servidor Airly", + "requests_per_day": "Solicita\u00e7\u00f5es permitidas por dia", + "requests_remaining": "Solicita\u00e7\u00f5es permitidas restantes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sk.json b/homeassistant/components/airly/translations/sk.json new file mode 100644 index 0000000000000..8e0bc629a1328 --- /dev/null +++ b/homeassistant/components/airly/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/manifest.json b/homeassistant/components/airnow/manifest.json index d4e7bc71937b9..583e23611efcf 100644 --- a/homeassistant/components/airnow/manifest.json +++ b/homeassistant/components/airnow/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/airnow", "requirements": ["pyairnow==1.1.0"], "codeowners": ["@asymworks"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyairnow"] } diff --git a/homeassistant/components/airnow/translations/el.json b/homeassistant/components/airnow/translations/el.json new file mode 100644 index 0000000000000..caaaafc9e9a38 --- /dev/null +++ b/homeassistant/components/airnow/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" + }, + "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_location": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (\u03bc\u03af\u03bb\u03b9\u03b1, \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + }, + "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 AirNow \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03ad\u03c1\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pt-BR.json b/homeassistant/components/airnow/translations/pt-BR.json new file mode 100644 index 0000000000000..fa24093b4192e --- /dev/null +++ b/homeassistant/components/airnow/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_location": "Nenhum resultado encontrado para esse local", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Raio da Esta\u00e7\u00e3o (milhas; opcional)" + }, + "description": "Configure a integra\u00e7\u00e3o da qualidade do ar AirNow. Para gerar a chave de API, acesse https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/sk.json b/homeassistant/components/airnow/translations/sk.json new file mode 100644 index 0000000000000..df686b2a56578 --- /dev/null +++ b/homeassistant/components/airnow/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/manifest.json b/homeassistant/components/airthings/manifest.json index 24585804b45f1..f3aba33ce80f6 100644 --- a/homeassistant/components/airthings/manifest.json +++ b/homeassistant/components/airthings/manifest.json @@ -7,5 +7,6 @@ "codeowners": [ "@danielhiversen" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["airthings"] } \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/el.json b/homeassistant/components/airthings/translations/el.json new file mode 100644 index 0000000000000..96ead31539288 --- /dev/null +++ b/homeassistant/components/airthings/translations/el.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 {url} \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2", + "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", + "secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/es-419.json b/homeassistant/components/airthings/translations/es-419.json index 952b27937dfcc..bdda51d409312 100644 --- a/homeassistant/components/airthings/translations/es-419.json +++ b/homeassistant/components/airthings/translations/es-419.json @@ -3,6 +3,8 @@ "step": { "user": { "data": { + "description": "Inicie sesi\u00f3n en {url} para encontrar sus credenciales", + "id": "ID", "secret": "Secreto" } } diff --git a/homeassistant/components/airthings/translations/pt-BR.json b/homeassistant/components/airthings/translations/pt-BR.json new file mode 100644 index 0000000000000..88693aaab0950 --- /dev/null +++ b/homeassistant/components/airthings/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "description": "Fa\u00e7a login em {url} para encontrar suas credenciais", + "id": "ID", + "secret": "Segredo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/sk.json b/homeassistant/components/airthings/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/airthings/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/manifest.json b/homeassistant/components/airtouch4/manifest.json index 8297081ae9dff..3e15f62710df2 100644 --- a/homeassistant/components/airtouch4/manifest.json +++ b/homeassistant/components/airtouch4/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@LonePurpleWolf" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["airtouch4pyapi"] } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/el.json b/homeassistant/components/airtouch4/translations/el.json index 004cb1a268f05..8790c6edab4ef 100644 --- a/homeassistant/components/airtouch4/translations/el.json +++ b/homeassistant/components/airtouch4/translations/el.json @@ -1,7 +1,17 @@ { "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", + "no_units": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1 AirTouch 4." + }, "step": { "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 {intergration}." } } diff --git a/homeassistant/components/airtouch4/translations/es-419.json b/homeassistant/components/airtouch4/translations/es-419.json new file mode 100644 index 0000000000000..1f4fd90ff08db --- /dev/null +++ b/homeassistant/components/airtouch4/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "no_units": "No se pudo encontrar ning\u00fan grupo de AirTouch 4." + }, + "step": { + "user": { + "title": "Configure los detalles de conexi\u00f3n de su AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/pt-BR.json b/homeassistant/components/airtouch4/translations/pt-BR.json new file mode 100644 index 0000000000000..e710bc87da07f --- /dev/null +++ b/homeassistant/components/airtouch4/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "no_units": "N\u00e3o foi poss\u00edvel encontrar nenhum Grupo AirTouch 4." + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + }, + "title": "Configure os detalhes de conex\u00e3o do AirTouch 4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 5d6a221dbbe0b..ed803a3e6a1cd 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/airvisual", "requirements": ["pyairvisual==5.0.9"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyairvisual", "pysmb"] } diff --git a/homeassistant/components/airvisual/translations/el.json b/homeassistant/components/airvisual/translations/el.json index cee03bf2677e5..bb4268a3ffd97 100644 --- a/homeassistant/components/airvisual/translations/el.json +++ b/homeassistant/components/airvisual/translations/el.json @@ -1,13 +1,61 @@ { "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 \u03ae \u03c4\u03bf Node/Pro ID \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\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" + }, "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", + "general_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "location_not_found": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + }, + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf AirVisual cloud API \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03bf\u03cd \u03c0\u03bb\u03ac\u03c4\u03bf\u03c5\u03c2/\u03bc\u03ae\u03ba\u03bf\u03c5\u03c2.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03af\u03b1\u03c2" + }, + "geography_by_name": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "city": "\u03a0\u03cc\u03bb\u03b7", + "country": "\u03a7\u03ce\u03c1\u03b1", + "state": "\u03ba\u03c1\u03ac\u03c4\u03bf\u03c2" + }, + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf AirVisual cloud API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1/\u03c7\u03ce\u03c1\u03b1.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03af\u03b1\u03c2" + }, + "node_pro": { + "data": { + "ip_address": "\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" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 AirVisual. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf UI \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 AirVisual Node/Pro" + }, "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, "title": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 AirVisual" }, "user": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd AirVisual \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7\u03c2 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf\u03bd \u03c7\u03ac\u03c1\u03c4\u03b7" + }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json index 733411f2465f9..b1d8b655029ec 100644 --- a/homeassistant/components/airvisual/translations/pt-BR.json +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -1,14 +1,62 @@ { "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, "error": { - "general_error": "Ocorreu um erro desconhecido.", - "invalid_api_key": "Chave de API fornecida \u00e9 inv\u00e1lida." + "cannot_connect": "Falha ao conectar", + "general_error": "Erro inesperado", + "invalid_api_key": "Chave de API inv\u00e1lida", + "location_not_found": "Localiza\u00e7\u00e3o n\u00e3o encontrada" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Use a API de nuvem AirVisual para monitorar uma latitude/longitude.", + "title": "Configurar uma geografia" + }, + "geography_by_name": { + "data": { + "api_key": "Chave da API", + "city": "Cidade", + "country": "Pa\u00eds", + "state": "Estado" + }, + "description": "Use a API de nuvem AirVisual para monitorar uma cidade/estado/pa\u00eds.", + "title": "Configurar uma geografia" + }, "node_pro": { "data": { + "ip_address": "Nome do host", "password": "Senha" - } + }, + "description": "Monitore uma unidade AirVisual pessoal. A senha pode ser recuperada da interface do usu\u00e1rio da unidade.", + "title": "Configurar um n\u00f3/pro AirVisual" + }, + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "title": "Reautenticar o AirVisual" + }, + "user": { + "description": "Escolha que tipo de dados do AirVisual voc\u00ea deseja monitorar.", + "title": "Configurar o Airvisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar o monitoramento no mapa" + }, + "title": "Configurar o AirVisual" } } } diff --git a/homeassistant/components/airvisual/translations/sensor.el.json b/homeassistant/components/airvisual/translations/sensor.el.json new file mode 100644 index 0000000000000..5367a2e0c869f --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.el.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u039c\u03bf\u03bd\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1", + "n2": "\u0394\u03b9\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", + "o3": "\u038c\u03b6\u03bf\u03bd", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u0394\u03b9\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5" + }, + "airvisual__pollutant_level": { + "good": "\u039a\u03b1\u03bb\u03cc", + "hazardous": "\u0395\u03c0\u03b9\u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf", + "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", + "unhealthy": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc", + "unhealthy_sensitive": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b5\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03b5\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2", + "very_unhealthy": "\u03a0\u03bf\u03bb\u03cd \u03b1\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.pt-BR.json b/homeassistant/components/airvisual/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..07ea1099cc1e5 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.pt-BR.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitrog\u00eanio", + "o3": "Oz\u00f4nio", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Di\u00f3xido de enxofre" + }, + "airvisual__pollutant_level": { + "good": "Bom", + "hazardous": "Perigoso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_sensitive": "Insalubre para grupos sens\u00edveis", + "very_unhealthy": "Muito insalubre" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sk.json b/homeassistant/components/airvisual/translations/sk.json new file mode 100644 index 0000000000000..22c02bbfec39f --- /dev/null +++ b/homeassistant/components/airvisual/translations/sk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "geography_by_coords": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + }, + "geography_by_name": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 6ab5c97d007e6..e28b2adff42b7 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": ["aladdin_connect==0.4"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aladdin_connect"] } diff --git a/homeassistant/components/alarm_control_panel/translations/el.json b/homeassistant/components/alarm_control_panel/translations/el.json index 450d01ff8a510..d01fe947a8e4d 100644 --- a/homeassistant/components/alarm_control_panel/translations/el.json +++ b/homeassistant/components/alarm_control_panel/translations/el.json @@ -4,13 +4,23 @@ "arm_away": "\u039f\u03c0\u03bb\u03af\u03c3\u03c4\u03b5 \u03c3\u03b5 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd \u03c4\u03bf {entity_name}", "arm_home": "\u039f\u03c0\u03bb\u03af\u03c3\u03c4\u03b5 \u03c3\u03b5 \u03c3\u03c0\u03af\u03c4\u03b9 \u03c4\u03bf {entity_name}", "arm_night": "\u039f\u03c0\u03bb\u03af\u03c3\u03c4\u03b5 \u03c3\u03b5 \u03b2\u03c1\u03ac\u03b4\u03c5 \u03c4\u03bf {entity_name}", + "arm_vacation": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 {entity_name} \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ad\u03c2", "disarm": "\u0391\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 {entity_name}", "trigger": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03ba\u03c4\u03cc\u03c2", + "is_armed_home": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c3\u03b5 \u03c3\u03c0\u03af\u03c4\u03b9", + "is_armed_night": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c3\u03b5 \u03bd\u03cd\u03c7\u03c4\u03b1", + "is_armed_vacation": "{entity_name} \u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ad\u03c2", + "is_disarmed": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf", + "is_triggered": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" + }, "trigger_type": { "armed_away": "{entity_name} \u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", "armed_home": "{entity_name} \u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03c3\u03c0\u03af\u03c4\u03b9", "armed_night": "{entity_name} \u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03cd\u03c7\u03c4\u03b1", + "armed_vacation": "{entity_name} \u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ad\u03c2", "disarmed": "{entity_name} \u03b1\u03c6\u03bf\u03c0\u03bb\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", "triggered": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } @@ -22,6 +32,7 @@ "armed_custom_bypass": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae", "armed_home": "\u03a3\u03c0\u03af\u03c4\u03b9 \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf", "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b2\u03c1\u03ac\u03b4\u03c5", + "armed_vacation": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd", "arming": "\u038c\u03c0\u03bb\u03b9\u03c3\u03b7", "disarmed": "\u0391\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", "disarming": "\u0391\u03c6\u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", diff --git a/homeassistant/components/alarm_control_panel/translations/pt-BR.json b/homeassistant/components/alarm_control_panel/translations/pt-BR.json index 07e005cba03e8..70d7a68ecc454 100644 --- a/homeassistant/components/alarm_control_panel/translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/translations/pt-BR.json @@ -4,13 +4,15 @@ "arm_away": "Armar {entity_name} longe", "arm_home": "Armar {entity_name} casa", "arm_night": "Armar {entity_name} noite", + "arm_vacation": "Armar {entity_name} f\u00e9rias", "disarm": "Desarmar {entity_name}", - "trigger": "Disparar {entidade_nome}" + "trigger": "Disparar {entity_name}" }, "condition_type": { "is_armed_away": "{entity_name} est\u00e1 armado modo longe", "is_armed_home": "{entity_name} est\u00e1 armadado modo casa", "is_armed_night": "{entity_name} est\u00e1 armadado modo noite", + "is_armed_vacation": "{entity_name} est\u00e1 armadado modo f\u00e9rias", "is_disarmed": "{entity_name} est\u00e1 desarmado", "is_triggered": "{entity_name} est\u00e1 acionado" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} armado modo longe", "armed_home": "{entity_name} armadado modo casa", "armed_night": "{entity_name} armadado para noite", + "armed_vacation": "{entity_name} armadado para f\u00e9rias", "disarmed": "{entity_name} desarmado", "triggered": "{entity_name} acionado" } @@ -27,8 +30,9 @@ "armed": "Armado", "armed_away": "Armado ausente", "armed_custom_bypass": "Armado em \u00e1reas espec\u00edficas", - "armed_home": "Armado casa", - "armed_night": "Armado noite", + "armed_home": "Armado em casa", + "armed_night": "Armado noturno", + "armed_vacation": "Armado f\u00e9rias", "arming": "Armando", "disarmed": "Desarmado", "disarming": "Desarmando", diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index fd0b76a5c8a77..3be3d67e32b3b 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -155,7 +155,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"]) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 12e97e16c62a6..87b6c86fc332d 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -77,6 +77,7 @@ def __init__( self._zone_number = int(zone_number) self._zone_type = zone_type self._attr_name = zone_name + self._attr_is_on = False self._rfid = zone_rfid self._loop = zone_loop self._relay_addr = relay_addr diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index a762d698545f1..c1f0401e2b030 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -3,7 +3,8 @@ "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": ["adext==0.4.2"], - "codeowners": ["@ajschmidt8"], + "codeowners": [], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["adext", "alarmdecoder"] } diff --git a/homeassistant/components/alarmdecoder/translations/el.json b/homeassistant/components/alarmdecoder/translations/el.json index 7c3b0b6737c08..4923b2eb0cd4f 100644 --- a/homeassistant/components/alarmdecoder/translations/el.json +++ b/homeassistant/components/alarmdecoder/translations/el.json @@ -6,6 +6,9 @@ "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf AlarmDecoder." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant/components/alarmdecoder/translations/es-419.json index d1f87c83cc894..d5124e40856e5 100644 --- a/homeassistant/components/alarmdecoder/translations/es-419.json +++ b/homeassistant/components/alarmdecoder/translations/es-419.json @@ -33,20 +33,26 @@ "init": { "data": { "edit_select": "Editar" - } + }, + "description": "\u00bfQu\u00e9 le gustar\u00eda editar?" }, "zone_details": { "data": { + "zone_loop": "Bucle de RF", "zone_name": "Nombre de zona", + "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n", + "zone_relaychan": "Canal de retransmisi\u00f3n", "zone_rfid": "Serie RF", "zone_type": "Tipo de zona" }, + "description": "Introduzca los detalles de la zona {zone_number}. Para eliminar la zona {zone_number}, deje el nombre de la zona en blanco.", "title": "Configurar AlarmDecoder" }, "zone_select": { "data": { "zone_number": "N\u00famero de zona" }, + "description": "Ingrese el n\u00famero de zona que le gustar\u00eda agregar, editar o eliminar.", "title": "Configurar AlarmDecoder" } } diff --git a/homeassistant/components/alarmdecoder/translations/pt-BR.json b/homeassistant/components/alarmdecoder/translations/pt-BR.json new file mode 100644 index 0000000000000..bdc65371ac7cd --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/pt-BR.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "create_entry": { + "default": "Conectado com sucesso ao AlarmDecoder." + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Taxa de transmiss\u00e3o do dispositivo", + "device_path": "Caminho do dispositivo", + "host": "Nome do host", + "port": "Porta" + }, + "title": "Definir as configura\u00e7\u00f5es de conex\u00e3o" + }, + "user": { + "data": { + "protocol": "Protocolo" + }, + "title": "Escolha o protocolo AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "O campo abaixo deve ser um n\u00famero inteiro.", + "loop_range": "RF Loop deve ser um n\u00famero inteiro entre 1 e 4.", + "loop_rfid": "RF Loop n\u00e3o pode ser usado sem RF Serial.", + "relay_inclusive": "O Relay Address e o Relay Channel s\u00e3o codependentes e devem ser inclu\u00eddos juntos." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Modo noturno alternativo", + "auto_bypass": "Auto Bypass ao armar", + "code_arm_required": "C\u00f3digo necess\u00e1rio para armar" + }, + "title": "Configurar AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Editar" + }, + "description": "O que voc\u00ea gostaria de editar?", + "title": "Configurar AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "Loop RF", + "zone_name": "Nome da zona", + "zone_relayaddr": "Endere\u00e7o do rel\u00e9", + "zone_relaychan": "Canal do rel\u00e9", + "zone_rfid": "RF Serial", + "zone_type": "Tipo de zona" + }, + "description": "Insira os detalhes da zona {zone_number}. Para excluir a zona {zone_number}, deixe o nome da zona em branco.", + "title": "Configurar AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero da zona" + }, + "description": "Insira o n\u00famero da zona que voc\u00ea deseja adicionar, editar ou remover.", + "title": "Configurar AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/sk.json b/homeassistant/components/alarmdecoder/translations/sk.json new file mode 100644 index 0000000000000..9b801344831e3 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "protocol": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 133ad4f2bda36..327d5973892f1 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -822,6 +822,8 @@ def get_valid_inputs(source_list): """Return list of supported inputs.""" input_list = [] for source in source_list: + if not isinstance(source, str): + continue formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") ) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 1ab24927bcb2c..5ecd326afb677 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -83,7 +83,7 @@ _LOGGER = logging.getLogger(__name__) -ENTITY_ADAPTERS = Registry() +ENTITY_ADAPTERS: Registry[str, type[AlexaEntity]] = Registry() TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f3f669de3b39c..a27bc432b4f34 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -73,7 +73,7 @@ from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() +HANDLERS = Registry() # type: ignore[var-annotated] @HANDLERS.register(("Alexa.Discovery", "Discover")) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index fede7d96810f2..7352bbd995ade 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -12,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() +HANDLERS = Registry() # type: ignore[var-annotated] INTENTS_API_ENDPOINT = "/api/alexa" @@ -166,7 +166,10 @@ async def async_handle_intent(hass, message): alexa_response.add_speech( alexa_speech, intent_response.speech[intent_speech]["speech"] ) - break + if intent_speech in intent_response.reprompt: + alexa_response.add_reprompt( + alexa_speech, intent_response.reprompt[intent_speech]["reprompt"] + ) if "simple" in intent_response.card: alexa_response.add_card( @@ -267,10 +270,9 @@ def add_reprompt(self, speech_type, text): key = "ssml" if speech_type == SpeechType.ssml else "text" - self.reprompt = { - "type": speech_type.value, - key: text.async_render(self.variables, parse_result=False), - } + self.should_end_session = False + + self.reprompt = {"type": speech_type.value, key: text} def as_dict(self): """Return response in an Alexa valid dict.""" diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json index cd045f25715c1..94203b46752f2 100644 --- a/homeassistant/components/almond/manifest.json +++ b/homeassistant/components/almond/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["http", "conversation"], "codeowners": ["@gcampax", "@balloob"], "requirements": ["pyalmond==0.0.2"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyalmond"] } diff --git a/homeassistant/components/almond/translations/el.json b/homeassistant/components/almond/translations/el.json index 716eb30c4a415..ac3a8efd757cd 100644 --- a/homeassistant/components/almond/translations/el.json +++ b/homeassistant/components/almond/translations/el.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "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} )", + "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." + }, "step": { "hassio_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf Almond \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf: {addon};", "title": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Almond \u03bc\u03ad\u03c3\u03c9 Home Assistant" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } } diff --git a/homeassistant/components/almond/translations/pt-BR.json b/homeassistant/components/almond/translations/pt-BR.json index 94dfbefb86a22..d012b1695f3a3 100644 --- a/homeassistant/components/almond/translations/pt-BR.json +++ b/homeassistant/components/almond/translations/pt-BR.json @@ -1,6 +1,16 @@ { "config": { + "abort": { + "cannot_connect": "Falha ao conectar", + "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})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao Almond fornecido pelo add-on {addon} ?", + "title": "Almond via add-on" + }, "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json index db96ef3d0a3ae..d1e0d1e1cb623 100644 --- a/homeassistant/components/almond/translations/uk.json +++ b/homeassistant/components/almond/translations/uk.json @@ -4,7 +4,7 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index 9606a440aab9a..d5139fcb8b812 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -4,7 +4,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "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})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index bfa41b3eeb15c..b608d18bb7a00 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": ["alpha_vantage==2.3.1"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["alpha_vantage"] } diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index b54fc9d918c1c..b8befe292eb43 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": ["boto3==1.20.24"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["boto3", "botocore", "s3transfer"] } diff --git a/homeassistant/components/ambee/translations/cs.json b/homeassistant/components/ambee/translations/cs.json new file mode 100644 index 0000000000000..6459ddb3ba0a6 --- /dev/null +++ b/homeassistant/components/ambee/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "step": { + "user": { + "data": { + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/el.json b/homeassistant/components/ambee/translations/el.json new file mode 100644 index 0000000000000..3576b6cd852fa --- /dev/null +++ b/homeassistant/components/ambee/translations/el.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "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", + "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": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Ambee." + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Ambee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/es-419.json b/homeassistant/components/ambee/translations/es-419.json index dee7d514b4865..de5ce971fa0c4 100644 --- a/homeassistant/components/ambee/translations/es-419.json +++ b/homeassistant/components/ambee/translations/es-419.json @@ -5,6 +5,9 @@ "data": { "description": "Vuelva a autenticarse con su cuenta de Ambee." } + }, + "user": { + "description": "Configure Ambee para que se integre con Home Assistant." } } } diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json new file mode 100644 index 0000000000000..2d960e17df248 --- /dev/null +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API", + "description": "Re-autentique com sua conta Ambee." + } + }, + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Configure o Ambee para integrar com o Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.el.json b/homeassistant/components/ambee/translations/sensor.el.json new file mode 100644 index 0000000000000..8e9af2dac056f --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.el.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", + "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", + "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pt-BR.json b/homeassistant/components/ambee/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..2e0dc187368e2 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.pt-BR.json @@ -0,0 +1,10 @@ +{ + "state": { + "ambee__risk": { + "high": "Alto", + "low": "Baixo", + "moderate": "Moderado", + "very high": "Muito alto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sk.json b/homeassistant/components/ambee/translations/sk.json new file mode 100644 index 0000000000000..a474631a7f858 --- /dev/null +++ b/homeassistant/components/ambee/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "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" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index 6dc79513e5529..a4fd72f5bdbe1 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -9,5 +9,6 @@ "requirements": [ "amberelectric==1.0.3" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["amberelectric"] } \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/el.json b/homeassistant/components/amberelectric/translations/el.json new file mode 100644 index 0000000000000..b6e939ea46689 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2", + "site_nmi": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 NMI" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf NMI \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5.", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API", + "site_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2" + }, + "description": "\u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf {api_url} \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/es-419.json b/homeassistant/components/amberelectric/translations/es-419.json index 7b82a2f08f74a..6cb3d42656e61 100644 --- a/homeassistant/components/amberelectric/translations/es-419.json +++ b/homeassistant/components/amberelectric/translations/es-419.json @@ -3,8 +3,17 @@ "step": { "site": { "data": { - "site_name": "Nombre del sitio" - } + "site_name": "Nombre del sitio", + "site_nmi": "NMI del sitio" + }, + "description": "Seleccione el NMI del sitio que le gustar\u00eda agregar" + }, + "user": { + "data": { + "api_token": "Token API", + "site_id": "ID del sitio" + }, + "description": "Vaya a {api_url} para generar una clave de API" } } } diff --git a/homeassistant/components/amberelectric/translations/pt-BR.json b/homeassistant/components/amberelectric/translations/pt-BR.json new file mode 100644 index 0000000000000..6e8eb4d0ac8ce --- /dev/null +++ b/homeassistant/components/amberelectric/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Nome do site", + "site_nmi": "Site NMI" + }, + "description": "Selecione o NMI do site que voc\u00ea gostaria de adicionar", + "title": "\u00c2mbar el\u00e9trico" + }, + "user": { + "data": { + "api_token": "Token de API", + "site_id": "ID do site" + }, + "description": "V\u00e1 para {api_url} para gerar uma chave de API", + "title": "\u00c2mbar el\u00e9trico" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 9441cdb86bc30..6e83f747bb1ac 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -6,5 +6,6 @@ "requirements": ["ambiclimate==0.2.1"], "dependencies": ["http"], "codeowners": ["@danielhiversen"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["ambiclimate"] } diff --git a/homeassistant/components/ambiclimate/translations/el.json b/homeassistant/components/ambiclimate/translations/el.json new file mode 100644 index 0000000000000..c2313d646f6d6 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03cc\u03bb\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", + "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", + "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." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "error": { + "follow_link": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03ba\u03b1\u03b9 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c0\u03c1\u03b9\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae", + "no_token": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Ambiclimate" + }, + "step": { + "auth": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd [\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf]({authorization_url}) \u03ba\u03b1\u03b9 **\u0395\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5** \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf \u0391\u03bc\u03c6\u03b9\u03ba\u03bb\u03b9\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 **\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae** \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.\n(\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 {cb_url})", + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/pt-BR.json b/homeassistant/components/ambiclimate/translations/pt-BR.json index 466096416ae66..19c0bce83cc88 100644 --- a/homeassistant/components/ambiclimate/translations/pt-BR.json +++ b/homeassistant/components/ambiclimate/translations/pt-BR.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "access_token": "Erro desconhecido ao gerar um token de acesso." + "access_token": "Erro desconhecido ao gerar um token de acesso.", + "already_configured": "A conta j\u00e1 foi configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." }, "create_entry": { - "default": "Autenticado com sucesso no Ambiclimate" + "default": "Autenticado com sucesso" }, "error": { "follow_link": "Por favor, siga o link e autentique-se antes de pressionar Enviar", @@ -12,7 +14,7 @@ }, "step": { "auth": { - "description": "Por favor, siga este [link]({authorization_url}) e Permitir acesso \u00e0 sua conta Ambiclimate, em seguida, volte e pressione Enviar abaixo. \n (Verifique se a URL de retorno de chamada especificada \u00e9 {cb_url})", + "description": "Por favor, siga este [link]({authorization_url}) e **Permitir** acesso \u00e0 sua conta Ambiclimate, em seguida, volte e pressione **Enviar** abaixo. \n (Verifique se a URL de retorno de chamada especificada \u00e9 {cb_url})", "title": "Autenticar Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/sk.json b/homeassistant/components/ambiclimate/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 33cb84706ff2f..21f7e25126946 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": ["aioambient==2021.11.0"], "codeowners": ["@bachya"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aioambient"] } diff --git a/homeassistant/components/ambient_station/translations/el.json b/homeassistant/components/ambient_station/translations/el.json new file mode 100644 index 0000000000000..0b69fc40b3ee8 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/el.json @@ -0,0 +1,20 @@ +{ + "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": { + "invalid_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "app_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" + }, + "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/it.json b/homeassistant/components/ambient_station/translations/it.json index 8984314349c65..ea3b9dcb95b69 100644 --- a/homeassistant/components/ambient_station/translations/it.json +++ b/homeassistant/components/ambient_station/translations/it.json @@ -11,7 +11,7 @@ "user": { "data": { "api_key": "Chiave API", - "app_key": "Application Key" + "app_key": "Chiave dell'applicazione" }, "title": "Inserisci i tuoi dati" } diff --git a/homeassistant/components/ambient_station/translations/pt-BR.json b/homeassistant/components/ambient_station/translations/pt-BR.json index d3ac36bf0e2f8..ce7a38d086747 100644 --- a/homeassistant/components/ambient_station/translations/pt-BR.json +++ b/homeassistant/components/ambient_station/translations/pt-BR.json @@ -1,13 +1,16 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { - "invalid_key": "Chave de API e / ou chave de aplicativo inv\u00e1lidas", + "invalid_key": "Chave de API inv\u00e1lida", "no_devices": "Nenhum dispositivo encontrado na conta" }, "step": { "user": { "data": { - "api_key": "Chave API", + "api_key": "Chave da API", "app_key": "Chave de aplicativo" }, "title": "Preencha suas informa\u00e7\u00f5es" diff --git a/homeassistant/components/ambient_station/translations/sk.json b/homeassistant/components/ambient_station/translations/sk.json new file mode 100644 index 0000000000000..01c13a4f11e89 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 1431b10a63872..5a7bec89e31b6 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -5,5 +5,6 @@ "requirements": ["amcrest==1.9.4"], "dependencies": ["ffmpeg"], "codeowners": ["@flacjacket"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["amcrest"] } diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index b47f84f2fe516..6c3978460e45a 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": ["asmog==0.0.6"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["asmog"] } diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index d5bc3db45f88b..ca4af7fd68a54 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -5,7 +5,7 @@ from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL +from homeassistant.components.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL from homeassistant.const import ( CONF_HOST, CONF_NAME, diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 9b9683856028d..157b618a2641d 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -125,8 +125,7 @@ def _migrate_aftv_entity(hass, aftv, entry_unique_id): # entity already exist, nothing to do return - old_unique_id = aftv.device_properties.get(PROP_SERIALNO) - if not old_unique_id: + if not (old_unique_id := aftv.device_properties.get(PROP_SERIALNO)): # serial no not found, exit return diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 89037a835caad..cd8e86a42a26e 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -9,5 +9,6 @@ ], "codeowners": ["@JeffLIrion", "@ollo69"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["adb_shell", "androidtv", "pure_python_adb"] } diff --git a/homeassistant/components/androidtv/translations/el.json b/homeassistant/components/androidtv/translations/el.json index 20c22238c1244..67db15ff17002 100644 --- a/homeassistant/components/androidtv/translations/el.json +++ b/homeassistant/components/androidtv/translations/el.json @@ -1,11 +1,15 @@ { "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", "invalid_unique_id": "\u0391\u03b4\u03cd\u03bd\u03b1\u03c4\u03bf\u03c2 \u03bf \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "error": { "adbkey_not_file": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd ADB \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", - "key_and_server": "\u03a0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af ADB \u03ae \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ADB" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_host": "\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", + "key_and_server": "\u03a0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af ADB \u03ae \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ADB", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { @@ -13,7 +17,9 @@ "adb_server_ip": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c4\u03b7 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5)", "adb_server_port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ADB", "adbkey": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", - "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" }, "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Android TV", "title": "Android TV" @@ -28,17 +34,31 @@ "apps": { "data": { "app_delete": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae", - "app_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" - } + "app_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", + "app_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 {app_id}", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd Android TV" }, "init": { + "data": { + "apps": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd", + "exclude_unnamed_apps": "\u0395\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd \u03bc\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c0\u03b7\u03b3\u03ce\u03bd", + "get_sources": "\u0391\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03ba\u03c4\u03b5\u03bb\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c0\u03b7\u03b3\u03ce\u03bd", + "screencap": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c3\u03cd\u03bb\u03bb\u03b7\u03c8\u03b7\u03c2 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03be\u03ce\u03c6\u03c5\u03bb\u03bb\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bb\u03bc\u03c0\u03bf\u03c5\u03bc", + "state_detection_rules": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03bd\u03cc\u03bd\u03c9\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "turn_off_command": "\u0395\u03bd\u03c4\u03bf\u03bb\u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03ba\u03b5\u03bb\u03cd\u03c6\u03bf\u03c5\u03c2 ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", + "turn_on_command": "\u0395\u03bd\u03c4\u03bf\u03bb\u03ae \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bb\u03cd\u03c6\u03bf\u03c5\u03c2 ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)" + }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Android TV" }, "rules": { "data": { "rule_delete": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03ba\u03b1\u03bd\u03cc\u03bd\u03b1", - "rule_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" + "rule_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", + "rule_values": "\u039b\u03af\u03c3\u03c4\u03b1 \u03ba\u03b1\u03bd\u03cc\u03bd\u03c9\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 (\u03b2\u03bb. \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7)" }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03bd\u03cc\u03bd\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 {rule_id}", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03bd\u03cc\u03bd\u03c9\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 Android TV" } } diff --git a/homeassistant/components/androidtv/translations/es-419.json b/homeassistant/components/androidtv/translations/es-419.json new file mode 100644 index 0000000000000..0e97b8c126576 --- /dev/null +++ b/homeassistant/components/androidtv/translations/es-419.json @@ -0,0 +1,53 @@ +{ + "config": { + "step": { + "user": { + "data": { + "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (dejar en blanco para no usar)", + "adb_server_port": "Puerto del servidor ADB", + "adbkey": "Ruta a su archivo de clave ADB (d\u00e9jelo en blanco para generarlo autom\u00e1ticamente)", + "device_class": "El tipo de dispositivo" + }, + "description": "Establezca los par\u00e1metros requeridos para conectarse a su dispositivo Android TV", + "title": "Android TV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Reglas de detecci\u00f3n de estado no v\u00e1lidas" + }, + "step": { + "apps": { + "data": { + "app_delete": "Marque para eliminar esta aplicaci\u00f3n", + "app_id": "ID de aplicaci\u00f3n", + "app_name": "Nombre de la aplicaci\u00f3n" + }, + "description": "Configurar la identificaci\u00f3n de la aplicaci\u00f3n {app_id}", + "title": "Configurar aplicaciones de Android TV" + }, + "init": { + "data": { + "apps": "Configurar lista de aplicaciones", + "exclude_unnamed_apps": "Excluir aplicaciones con nombre desconocido de la lista de fuentes", + "get_sources": "Recuperar las aplicaciones en ejecuci\u00f3n como la lista de fuentes", + "screencap": "Usar captura de pantalla para la car\u00e1tula del \u00e1lbum", + "state_detection_rules": "Configurar reglas de detecci\u00f3n de estado", + "turn_off_command": "Comando de apagado de shell ADB (d\u00e9jelo vac\u00edo por defecto)", + "turn_on_command": "Comando de activaci\u00f3n de shell ADB (d\u00e9jelo vac\u00edo por defecto)" + }, + "title": "Opciones de Android TV" + }, + "rules": { + "data": { + "rule_delete": "Marque para eliminar esta regla", + "rule_id": "ID de aplicaci\u00f3n", + "rule_values": "Lista de reglas de detecci\u00f3n de estado (ver documentaci\u00f3n)" + }, + "description": "Configure la regla de detecci\u00f3n para la identificaci\u00f3n de la aplicaci\u00f3n {rule_id}", + "title": "Configurar reglas de detecci\u00f3n de estado de Android TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/pt-BR.json b/homeassistant/components/androidtv/translations/pt-BR.json index bccf453eb0b7b..86e9a29dd12d6 100644 --- a/homeassistant/components/androidtv/translations/pt-BR.json +++ b/homeassistant/components/androidtv/translations/pt-BR.json @@ -1,10 +1,65 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo" + }, + "error": { + "adbkey_not_file": "Arquivo de chave ADB n\u00e3o encontrado", + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "key_and_server": "Forne\u00e7a apenas chave ADB ou servidor ADB", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { - "device_class": "O tipo de dispositivo" - } + "adb_server_ip": "Endere\u00e7o IP do servidor ADB (deixe em branco para n\u00e3o usar)", + "adb_server_port": "Porta do servidor ADB", + "adbkey": "Caminho para o arquivo de chave ADB (deixe em branco para gerar automaticamente)", + "device_class": "O tipo de dispositivo", + "host": "Nome do host", + "port": "Porta" + }, + "description": "Defina os par\u00e2metros necess\u00e1rios para se conectar ao seu dispositivo Android TV", + "title": "AndroidTV" + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Regras de detec\u00e7\u00e3o de estado inv\u00e1lidas" + }, + "step": { + "apps": { + "data": { + "app_delete": "Marque para excluir este aplicativo", + "app_id": "ID do aplicativo", + "app_name": "Nome do aplicativo" + }, + "description": "Configurar o ID do aplicativo {app_id}", + "title": "Configurar aplicativos da Android TV" + }, + "init": { + "data": { + "apps": "Configurar lista de aplicativos", + "exclude_unnamed_apps": "Excluir aplicativos com nome desconhecido da lista de fontes", + "get_sources": "Recupere os aplicativos em execu\u00e7\u00e3o como a lista de fontes", + "screencap": "Use a captura de tela para a arte do \u00e1lbum", + "state_detection_rules": "Configurar regras de detec\u00e7\u00e3o de estado", + "turn_off_command": "Comando de desligamento do shell ADB (deixe vazio por padr\u00e3o)", + "turn_on_command": "Comando de ativa\u00e7\u00e3o do shell ADB (deixe vazio por padr\u00e3o)" + }, + "title": "Op\u00e7\u00f5es de TV Android" + }, + "rules": { + "data": { + "rule_delete": "Marque para excluir esta regra", + "rule_id": "ID do aplicativo", + "rule_values": "Lista de regras de detec\u00e7\u00e3o de estado (consulte a documenta\u00e7\u00e3o)" + }, + "description": "Configure a regra de detec\u00e7\u00e3o para o ID do aplicativo {rule_id}", + "title": "Configurar regras de detec\u00e7\u00e3o de estado do Android TV" } } } diff --git a/homeassistant/components/androidtv/translations/sk.json b/homeassistant/components/androidtv/translations/sk.json new file mode 100644 index 0000000000000..86bba63ac4967 --- /dev/null +++ b/homeassistant/components/androidtv/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 926549f768d82..49c7f3985e50a 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["anel_pwrctrl"] } diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 078ecaae0da1e..c43b976416d91 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.2.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["anthemav"] } diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 688c7c9fb3d7a..3b290146a0941 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": ["aiokafka==0.6.0"], "codeowners": ["@bachya"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiokafka", "kafka_python"] } diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 7cbf33f8b47ae..a032430e1bc06 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,4 +1,5 @@ """Support for APCUPSd via its Network Information Server (NIS).""" +# pylint: disable=import-error from datetime import timedelta import logging diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index ac9352bae449b..13a08685c68af 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["apcaccess"] } diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index b7e7366796b83..2fae17ac922e6 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,4 +1,5 @@ """Support for APCUPSd sensors.""" +# pylint: disable=import-error from __future__ import annotations import logging diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 73136a2ff2982..bcefdcf063945 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -1,9 +1,11 @@ { + "disabled": "Integration library not compatible with Python 3.10", "domain": "apns", "name": "Apple Push Notification Service (APNS)", "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": ["apns2==0.3.0"], "after_dependencies": ["device_tracker"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["apns2", "hyper"] } diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 4cc13a3057fb6..8d0dcc334e94d 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,4 +1,5 @@ """APNS Notification platform.""" +# pylint: disable=import-error from contextlib import suppress import logging diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index bd511d84eb536..d61c21972fbe1 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -242,7 +242,7 @@ async def _connect_loop(self): backoff = min( max( BACKOFF_TIME_LOWER_LIMIT, - randrange(2 ** self._connection_attempts), + randrange(2**self._connection_attempts), ), BACKOFF_TIME_UPPER_LIMIT, ) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index b3af0413bf1e4..03d662b97217b 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -21,5 +21,6 @@ {"type":"_raop._tcp.local.", "properties": {"am":"airport*"}} ], "codeowners": ["@postlund"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyatv", "srptools"] } diff --git a/homeassistant/components/apple_tv/translations/el.json b/homeassistant/components/apple_tv/translations/el.json index 37b07361ad838..11a61899b8437 100644 --- a/homeassistant/components/apple_tv/translations/el.json +++ b/homeassistant/components/apple_tv/translations/el.json @@ -1,15 +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", + "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", + "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", "backoff": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae (\u03af\u03c3\u03c9\u03c2 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03b5\u03b9 \u03ac\u03ba\u03c5\u03c1\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c6\u03bf\u03c1\u03ad\u03c2), \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", "device_did_not_pair": "\u0394\u03b5\u03bd \u03ad\u03b3\u03b9\u03bd\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "device_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "inconsistent_device": "\u03a4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u0391\u03c5\u03c4\u03cc \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c5\u03c0\u03bf\u03b4\u03b7\u03bb\u03ce\u03bd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03bc\u03b5 \u03c4\u03bf multicast DNS (Zeroconf). \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "invalid_config": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", - "setup_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2." + "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", + "setup_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { - "no_usable_service": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae, \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03bd\u03ad\u03bd\u03b1\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd. \u0391\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03bb\u03ad\u03c0\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Apple TV." + "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", + "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", + "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", + "no_usable_service": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae, \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03bd\u03ad\u03bd\u03b1\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd. \u0391\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03bb\u03ad\u03c0\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Apple TV.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name} ({type})", "step": { @@ -22,6 +32,9 @@ "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" }, "pair_with_pin": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, "description": "\u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf `{protocol}`. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03bf\u03b8\u03cc\u03bd\u03b7. \u03a4\u03b1 \u03c0\u03c1\u03ce\u03c4\u03b1 \u03bc\u03b7\u03b4\u03b5\u03bd\u03b9\u03ba\u03ac \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bb\u03b5\u03af\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9, \u03c0.\u03c7. \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 123 \u03b5\u03ac\u03bd \u03bf \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 0123.", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" }, @@ -30,7 +43,8 @@ "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "protocol_disabled": { - "description": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf `{protocol}` \u03b1\u03bb\u03bb\u03ac \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03b9\u03b8\u03b1\u03bd\u03bf\u03cd\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03c0.\u03c7. \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03c3\u03b5 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9) \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c7\u03c9\u03c1\u03af\u03c2 \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5, \u03b1\u03bb\u03bb\u03ac \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b5\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2." + "description": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf `{protocol}` \u03b1\u03bb\u03bb\u03ac \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03b9\u03b8\u03b1\u03bd\u03bf\u03cd\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03c0.\u03c7. \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03c3\u03b5 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9) \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c7\u03c9\u03c1\u03af\u03c2 \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5, \u03b1\u03bb\u03bb\u03ac \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b5\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2.", + "title": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" }, "reconfigure": { "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc\u03c4\u03b7\u03c4\u03ac \u03c4\u03b7\u03c2.", @@ -52,6 +66,9 @@ "options": { "step": { "init": { + "data": { + "start_off": "\u039c\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant" + }, "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b3\u03b5\u03bd\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } } diff --git a/homeassistant/components/apple_tv/translations/pt-BR.json b/homeassistant/components/apple_tv/translations/pt-BR.json new file mode 100644 index 0000000000000..79fee5f02ec39 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/pt-BR.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "backoff": "O dispositivo n\u00e3o aceita solicita\u00e7\u00f5es de emparelhamento neste momento (voc\u00ea pode ter digitado um c\u00f3digo PIN inv\u00e1lido muitas vezes), tente novamente mais tarde.", + "device_did_not_pair": "Nenhuma tentativa de concluir o processo de emparelhamento foi feita a partir do dispositivo.", + "device_not_found": "O dispositivo n\u00e3o foi encontrado durante a descoberta. Tente adicion\u00e1-lo novamente.", + "inconsistent_device": "Os protocolos esperados n\u00e3o foram encontrados durante a descoberta. Isso normalmente indica um problema com o DNS multicast (Zeroconf). Tente adicionar o dispositivo novamente.", + "invalid_config": "A configura\u00e7\u00e3o deste dispositivo est\u00e1 incompleta. Tente adicion\u00e1-lo novamente.", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "setup_failed": "Falha ao configurar o dispositivo.", + "unknown": "Erro inesperado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_usable_service": "Um dispositivo foi encontrado, mas n\u00e3o foi poss\u00edvel identificar nenhuma maneira de estabelecer uma conex\u00e3o com ele. Se voc\u00ea continuar vendo esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a Apple TV.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({type})", + "step": { + "confirm": { + "description": "Voc\u00ea est\u00e1 prestes a adicionar `{name}` do tipo `{type}` ao Home Assistant.\n\n** Para completar o processo, voc\u00ea pode ter que inserir v\u00e1rios c\u00f3digos PIN.**\n\nObserve que voc\u00ea *n\u00e3o* poder\u00e1 desligar sua TV Apple com esta integra\u00e7\u00e3o. Somente o reprodutor de m\u00eddia no Home Assistant ser\u00e1 desligado!", + "title": "Confirme a adi\u00e7\u00e3o da Apple TV" + }, + "pair_no_pin": { + "description": "O emparelhamento \u00e9 necess\u00e1rio para o servi\u00e7o `{protocol}`. Digite PIN {pin} no dispositivo para continuar.", + "title": "Emparelhamento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "O emparelhamento \u00e9 necess\u00e1rio para o protocolo `{protocol}`. Digite o c\u00f3digo PIN exibido na tela. Os zeros principais ser\u00e3o omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", + "title": "Emparelhamento" + }, + "password": { + "description": "Uma senha \u00e9 exigida por `{protocol}`. Isso ainda n\u00e3o est\u00e1 suportado, por favor desabilitar a senha para continuar.", + "title": "Senha requerida" + }, + "protocol_disabled": { + "description": "O emparelhamento \u00e9 necess\u00e1rio para ` {protocol} ` mas est\u00e1 desabilitado no dispositivo. Revise as poss\u00edveis restri\u00e7\u00f5es de acesso (por exemplo, permitir que todos os dispositivos da rede local se conectem) no dispositivo. \n\n Voc\u00ea pode continuar sem emparelhar este protocolo, mas algumas funcionalidades ser\u00e3o limitadas.", + "title": "N\u00e3o \u00e9 poss\u00edvel emparelhar" + }, + "reconfigure": { + "description": "Reconfigure este dispositivo para restaurar sua funcionalidade.", + "title": "Reconfigura\u00e7\u00e3o do dispositivo" + }, + "service_problem": { + "description": "Ocorreu um problema ao emparelhar o protocolo `{protocol}`. Ser\u00e1 ignorado.", + "title": "Falha ao adicionar servi\u00e7o" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Comece digitando o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que voc\u00ea deseja adicionar. \n\n Se voc\u00ea n\u00e3o conseguir ver seu dispositivo ou tiver problemas, tente especificar o endere\u00e7o IP do dispositivo.", + "title": "Configurar uma nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N\u00e3o ligue o dispositivo ao iniciar o Home Assistant" + }, + "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sk.json b/homeassistant/components/apple_tv/translations/sk.json new file mode 100644 index 0000000000000..e0e6b1c5bda91 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 4e0209cc337db..21e2ed7c94d61 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,8 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.6"], + "requirements": ["apprise==0.9.7"], "codeowners": ["@caronc"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["apprise"] } diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index dc29ff6fff542..6979eab4516d4 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aprs", "codeowners": ["@PhilRW"], "requirements": ["aprslib==0.7.0", "geopy==2.1.0"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aprslib", "geographiclib", "geopy"] } diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index acae105b54d6f..9181118900001 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": ["aqualogic==2.6"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aqualogic"] } diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index a28c852d8dbcb..b0da88a845063 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": ["sharp_aquos_rc==0.3.2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["sharp_aquos_rc"] } diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index b33710bf936b3..2b1a3bf3a1958 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -76,7 +76,7 @@ def _handle_event(event: Event): job, { "trigger": { - **trigger_data, # type: ignore # https://github.com/python/mypy/issues/9117 + **trigger_data, # type: ignore[arg-type] # https://github.com/python/mypy/issues/9117 **config, "description": f"{DOMAIN} - {entity_id}", } diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 08545f4c5b057..c91c92922b494 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -11,5 +11,6 @@ } ], "codeowners": ["@elupus"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["arcam"] } diff --git a/homeassistant/components/arcam_fmj/translations/el.json b/homeassistant/components/arcam_fmj/translations/el.json new file mode 100644 index 0000000000000..bc639192d8604 --- /dev/null +++ b/homeassistant/components/arcam_fmj/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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Arcam FMJ \u03c3\u03c4\u03bf `{host}` \u03c3\u03c4\u03bf Home Assistant;" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \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 \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0396\u03b7\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc {entity_name} \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/es-419.json b/homeassistant/components/arcam_fmj/translations/es-419.json index a69b353354b74..78655f3ac42f4 100644 --- a/homeassistant/components/arcam_fmj/translations/es-419.json +++ b/homeassistant/components/arcam_fmj/translations/es-419.json @@ -8,5 +8,10 @@ "description": "Ingrese el nombre de host o la direcci\u00f3n IP del dispositivo." } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} ha sido solicitada para encender" + } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/pt-BR.json b/homeassistant/components/arcam_fmj/translations/pt-BR.json index 8071efb001f35..8df3d821f934f 100644 --- a/homeassistant/components/arcam_fmj/translations/pt-BR.json +++ b/homeassistant/components/arcam_fmj/translations/pt-BR.json @@ -1,4 +1,24 @@ { + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{host}", + "step": { + "confirm": { + "description": "Deseja adicionar Arcam FMJ em `{host}` ao Home Assistant?" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "description": "Digite o nome do host ou o endere\u00e7o IP do dispositivo." + } + } + }, "device_automation": { "trigger_type": { "turn_on": "Foi solicitado que {entity_name} ligue" diff --git a/homeassistant/components/arcam_fmj/translations/sk.json b/homeassistant/components/arcam_fmj/translations/sk.json new file mode 100644 index 0000000000000..b41d6edbd4b1e --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 7b4978b56c1cd..5ba5180b91410 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyarlo", "sseclient_py"] } diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 01da8b8af3ca8..63d292d54accb 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", "requirements": ["arris-tg2492lg==1.2.1"], "codeowners": ["@vanbalken"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["arris_tg2492lg"] } diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index 660ba9f06f13c..4b72a12aa2681 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": ["pexpect==4.6.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pexpect", "ptyprocess"] } diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 697157bd86686..213d0dabc91a9 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[str] = [Platform.SENSOR] +PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py new file mode 100644 index 0000000000000..f67ea58bfc4d2 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Aseko Pool Live binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from aioaseko import Unit + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AsekoDataUpdateCoordinator +from .const import DOMAIN +from .entity import AsekoEntity + + +@dataclass +class AsekoBinarySensorDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Unit], bool] + + +@dataclass +class AsekoBinarySensorEntityDescription( + BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin +): + """Describes a Aseko binary sensor entity.""" + + +UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( + AsekoBinarySensorEntityDescription( + key="water_flow", + name="Water Flow", + icon="mdi:waves-arrow-right", + value_fn=lambda unit: unit.water_flow, + ), + AsekoBinarySensorEntityDescription( + key="has_alarm", + name="Alarm", + value_fn=lambda unit: unit.has_alarm, + device_class=BinarySensorDeviceClass.SAFETY, + ), + AsekoBinarySensorEntityDescription( + key="has_error", + name="Error", + value_fn=lambda unit: unit.has_error, + device_class=BinarySensorDeviceClass.PROBLEM, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aseko Pool Live binary sensors.""" + data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ + config_entry.entry_id + ] + entities: list[BinarySensorEntity] = [] + for unit, coordinator in data: + for description in UNIT_BINARY_SENSORS: + entities.append(AsekoUnitBinarySensorEntity(unit, coordinator, description)) + async_add_entities(entities) + + +class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): + """Representation of a unit water flow binary sensor entity.""" + + entity_description: AsekoBinarySensorEntityDescription + + def __init__( + self, + unit: Unit, + coordinator: AsekoDataUpdateCoordinator, + entity_description: AsekoBinarySensorEntityDescription, + ) -> None: + """Initialize the unit binary sensor.""" + super().__init__(unit, coordinator) + self.entity_description = entity_description + self._attr_name = f"{self._device_name} {entity_description.name}" + self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._unit) diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index d1fc553a9ffb8..3b5b994282dff 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -7,5 +7,6 @@ "codeowners": [ "@milanmeu" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aioaseko"] } \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/el.json b/homeassistant/components/aseko_pool_live/translations/el.json new file mode 100644 index 0000000000000..417f896375fb6 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/el.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/pt-BR.json b/homeassistant/components/aseko_pool_live/translations/pt-BR.json new file mode 100644 index 0000000000000..bb8bb1f781987 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + } + } + } + } +} \ 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 new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index 068da7d64f44c..d42233ffa2d6c 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": ["asterisk_mbox==0.5.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["asterisk_mbox"] } diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 7d3ea839ebd29..f735e520bdc3a 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,161 +1,49 @@ """Support for ASUSWRT devices.""" -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MODE, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_SENSORS, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, - Platform, -) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_DNSMASQ, - CONF_INTERFACE, - CONF_REQUIRE_IP, - CONF_SSH_KEY, - DATA_ASUSWRT, - DEFAULT_DNSMASQ, - DEFAULT_INTERFACE, - DEFAULT_SSH_PORT, - DOMAIN, - MODE_AP, - MODE_ROUTER, - PROTOCOL_SSH, - PROTOCOL_TELNET, -) + +from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] -CONF_PUB_KEY = "pub_key" -SECRET_GROUP = "Password or SSH Key" -SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PROTOCOL, default=PROTOCOL_SSH): vol.In( - [PROTOCOL_SSH, PROTOCOL_TELNET] - ), - vol.Optional(CONF_MODE, default=MODE_ROUTER): vol.In( - [MODE_ROUTER, MODE_AP] - ), - vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, - vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, - vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, - vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, - vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile, - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, - vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the AsusWrt integration.""" - if (conf := config.get(DOMAIN)) is None: - return True - - # save the options from config yaml - options = {} - mode = conf.get(CONF_MODE, MODE_ROUTER) - for name, value in conf.items(): - if name in [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]: - if name == CONF_REQUIRE_IP and mode != MODE_AP: - continue - options[name] = value - hass.data[DOMAIN] = {"yaml_options": options} - - # check if already configured - domains_list = hass.config_entries.async_domains() - if DOMAIN in domains_list: - return True - - # remove not required config keys - if pub_key := conf.pop(CONF_PUB_KEY, ""): - conf[CONF_SSH_KEY] = pub_key - - conf.pop(CONF_REQUIRE_IP, True) - conf.pop(CONF_SENSORS, {}) - conf.pop(CONF_INTERFACE, "") - conf.pop(CONF_DNSMASQ, "") - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AsusWrt platform.""" - # import options from yaml if empty - yaml_options = hass.data.get(DOMAIN, {}).pop("yaml_options", {}) - if not entry.options and yaml_options: - hass.config_entries.async_update_entry(entry, options=yaml_options) - router = AsusWrtRouter(hass, entry) await router.setup() router.async_on_close(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - async def async_close_connection(event): """Close AsusWrt connection on HA Stop.""" await router.close() - stop_listener = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_close_connection + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - DATA_ASUSWRT: router, - "stop_listener": stop_listener, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ASUSWRT: router} + + hass.config_entries.async_setup_platforms(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: - hass.data[DOMAIN][entry.entry_id]["stop_listener"]() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] await router.close() - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update when config_entry options update.""" router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index 5a20880b4b020..a06071a33d19b 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -165,10 +165,6 @@ async def async_step_user(self, user_input=None): data=user_input, ) - async def async_step_import(self, user_input=None): - """Import a config entry.""" - return await self.async_step_user(user_input) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/asuswrt/diagnostics.py b/homeassistant/components/asuswrt/diagnostics.py new file mode 100644 index 0000000000000..dc26bca551291 --- /dev/null +++ b/homeassistant/components/asuswrt/diagnostics.py @@ -0,0 +1,84 @@ +"""Diagnostics support for Asuswrt.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import DATA_ASUSWRT, DOMAIN +from .router import AsusWrtRouter + +TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} + + router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + + # Gather information how this AsusWrt device is represented in Home Assistant + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + hass_device = device_registry.async_get_device( + identifiers=router.device_info["identifiers"] + ) + if not hass_device: + return data + + data["device"] = { + "name": hass_device.name, + "name_by_user": hass_device.name_by_user, + "disabled": hass_device.disabled, + "disabled_by": hass_device.disabled_by, + "device_info": async_redact_data(dict(router.device_info), {"identifiers"}), + "entities": {}, + "tracked_devices": [], + } + + hass_entities = er.async_entries_for_device( + entity_registry, + device_id=hass_device.id, + include_disabled_entities=True, + ) + + for entity_entry in hass_entities: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + # The entity_id is already provided at root level. + state_dict.pop("entity_id", None) + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) + + data["device"]["entities"][entity_entry.entity_id] = { + "name": entity_entry.name, + "original_name": entity_entry.original_name, + "disabled": entity_entry.disabled, + "disabled_by": entity_entry.disabled_by, + "entity_category": entity_entry.entity_category, + "device_class": entity_entry.device_class, + "original_device_class": entity_entry.original_device_class, + "icon": entity_entry.icon, + "original_icon": entity_entry.original_icon, + "unit_of_measurement": entity_entry.unit_of_measurement, + "state": state_dict, + } + + for device in router.devices.values(): + data["device"]["tracked_devices"].append( + { + "name": device.name or "Unknown device", + "ip_address": device.ip_address, + "last_activity": device.last_activity, + } + ) + + return data diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 1470c075b0438..c1d67fa9e5768 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.4.0"], "codeowners": ["@kennedyshead", "@ollo69"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aioasuswrt", "asyncssh"] } diff --git a/homeassistant/components/asuswrt/translations/el.json b/homeassistant/components/asuswrt/translations/el.json index e2b2ea2b4b089..ea926f10bd275 100644 --- a/homeassistant/components/asuswrt/translations/el.json +++ b/homeassistant/components/asuswrt/translations/el.json @@ -1,14 +1,27 @@ { "config": { + "abort": { + "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", + "invalid_host": "\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", "pwd_and_ssh": "\u03a0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ae \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH", "pwd_or_ssh": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ae \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH", - "ssh_not_file": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" + "ssh_not_file": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { "data": { - "ssh_key": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH (\u03b1\u03bd\u03c4\u03af \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "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", + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7", + "ssh_key": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd SSH (\u03b1\u03bd\u03c4\u03af \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2", "title": "AsusWRT" @@ -22,7 +35,8 @@ "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2 \u03c0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03bc\u03ac\u03ba\u03c1\u03c5\u03bd\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "dnsmasq": "\u0397 \u03b8\u03ad\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c4\u03c9\u03bd \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd dnsmasq.leases", "interface": "\u0397 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 (\u03c0.\u03c7. eth0, eth1 \u03ba.\u03bb\u03c0.)", - "require_ip": "\u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd IP (\u03b3\u03b9\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)" + "require_ip": "\u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd IP (\u03b3\u03b9\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)", + "track_unknown": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03c9\u03bd / \u03b1\u03bd\u03ce\u03bd\u03c5\u03bc\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 AsusWRT" } diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json new file mode 100644 index 0000000000000..06982ade6227b --- /dev/null +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "pwd_and_ssh": "Forne\u00e7a apenas senha ou arquivo de chave SSH", + "pwd_or_ssh": "Forne\u00e7a a senha ou o arquivo de chave SSH", + "ssh_not_file": "Arquivo de chave SSH n\u00e3o encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "mode": "Modo", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "protocol": "Protocolo de comunica\u00e7\u00e3o a ser usado", + "ssh_key": "Caminho para seu arquivo de chave SSH (em vez de senha)", + "username": "Usu\u00e1rio" + }, + "description": "Defina o par\u00e2metro necess\u00e1rio para se conectar ao seu roteador", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segundos para esperar antes de considerar um dispositivo ausente", + "dnsmasq": "A localiza\u00e7\u00e3o dos arquivos dnsmasq.leases no roteador", + "interface": "A interface da qual voc\u00ea deseja estat\u00edsticas (por exemplo, eth0,eth1 etc)", + "require_ip": "Os dispositivos devem ter IP (para o modo de ponto de acesso)", + "track_unknown": "Rastrear dispositivos desconhecidos/sem nome" + }, + "title": "Op\u00e7\u00f5es AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/sk.json b/homeassistant/components/asuswrt/translations/sk.json new file mode 100644 index 0000000000000..39d2e182c40be --- /dev/null +++ b/homeassistant/components/asuswrt/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index 7aabf592ee383..d0997e495c5fa 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index eb9dc54ecd289..39e483721677b 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/atag/", "requirements": ["pyatag==0.3.5.3"], "codeowners": ["@MatsNL"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyatag"] } diff --git a/homeassistant/components/atag/translations/el.json b/homeassistant/components/atag/translations/el.json index e00e4c23097d4..f3f47f1845074 100644 --- a/homeassistant/components/atag/translations/el.json +++ b/homeassistant/components/atag/translations/el.json @@ -1,7 +1,20 @@ { "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", "unauthorized": "\u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b5, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\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", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } } } } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/it.json b/homeassistant/components/atag/translations/it.json index 1bc473a60018c..46fbbc3d97250 100644 --- a/homeassistant/components/atag/translations/it.json +++ b/homeassistant/components/atag/translations/it.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Porta" }, - "title": "Connettersi al dispositivo" + "title": "Connettiti al dispositivo" } } } diff --git a/homeassistant/components/atag/translations/pt-BR.json b/homeassistant/components/atag/translations/pt-BR.json index 5d9d507911090..1577ad4436ff5 100644 --- a/homeassistant/components/atag/translations/pt-BR.json +++ b/homeassistant/components/atag/translations/pt-BR.json @@ -1,7 +1,20 @@ { "config": { "abort": { - "already_configured": "Este dispositivo j\u00e1 foi adicionado ao Home Assistant" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unauthorized": "Emparelhamento negado, verifique o dispositivo para solicita\u00e7\u00e3o de autentica\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "title": "Conecte-se ao dispositivo" + } } } } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/sk.json b/homeassistant/components/atag/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/atag/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 975e7f1ac3165..415cb900dc2b6 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/atome", "codeowners": ["@baqs"], "requirements": ["pyatome==0.1.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyatome"] } diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py new file mode 100644 index 0000000000000..d7f2a5ba4aed0 --- /dev/null +++ b/homeassistant/components/august/button.py @@ -0,0 +1,39 @@ +"""Support for August buttons.""" +from yalexs.lock import Lock + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AugustData +from .const import DATA_AUGUST, DOMAIN +from .entity import AugustEntityMixin + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up August lock wake buttons.""" + data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + async_add_entities([AugustWakeLockButton(data, lock) for lock in data.locks]) + + +class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): + """Representation of an August lock wake button.""" + + def __init__(self, data: AugustData, device: Lock) -> None: + """Initialize the lock wake button.""" + super().__init__(data, device) + self._attr_name = f"{device.device_name} Wake" + self._attr_unique_id = f"{self._device_id}_wake" + + async def async_press(self, **kwargs): + """Wake the device.""" + await self._data.async_status_async(self._device_id, self._hyper_bridge) + + @callback + def _update_from_data(self): + """Nothing to update as buttons are stateless.""" diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index ce1ede865384f..268894275552d 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -37,8 +37,6 @@ class AugustCamera(AugustEntityMixin, Camera): def __init__(self, data, device, session, timeout): """Initialize a August security camera.""" super().__init__(data, device) - self._data = data - self._device = device self._timeout = timeout self._session = session self._image_url = None diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index ae3fcdf90e3cb..ac6f463467f47 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -46,6 +46,7 @@ LOGIN_METHODS = ["phone", "email"] PLATFORMS = [ + Platform.BUTTON, Platform.CAMERA, Platform.BINARY_SENSOR, Platform.LOCK, diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index a0fe44838c227..209747da0bee7 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -36,6 +36,11 @@ def _device_id(self): def _detail(self): return self._data.get_device_detail(self._device.device_id) + @property + def _hyper_bridge(self): + """Check if the lock has a paired hyper bridge.""" + return bool(self._detail.bridge and self._detail.bridge.hyper_bridge) + @callback def _update_from_data_and_write_state(self): self._update_from_data() diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 41abc1c6aa287..e30f8301a8f6d 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -39,18 +39,11 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): def __init__(self, data, device): """Initialize the lock.""" super().__init__(data, device) - self._data = data - self._device = device self._lock_status = None self._attr_name = device.device_name self._attr_unique_id = f"{self._device_id:s}_lock" self._update_from_data() - @property - def _hyper_bridge(self): - """Check if the lock has a paired hyper bridge.""" - return bool(self._detail.bridge and self._detail.bridge.hyper_bridge) - async def async_lock(self, **kwargs): """Lock the device.""" if self._data.activity_stream.pubnub.connected: diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 0dfcb6094b186..1eb923b91a8af 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -23,5 +23,6 @@ } ], "config_flow": true, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pubnub", "yalexs"] } diff --git a/homeassistant/components/august/translations/el.json b/homeassistant/components/august/translations/el.json index 1ee7a68fa87be..a8516ec8041cb 100644 --- a/homeassistant/components/august/translations/el.json +++ b/homeassistant/components/august/translations/el.json @@ -1,5 +1,14 @@ { "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", + "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", + "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": { "reauth_validate": { "data": { diff --git a/homeassistant/components/august/translations/es-419.json b/homeassistant/components/august/translations/es-419.json index 7e5fe76d3afbc..efb55133c9c75 100644 --- a/homeassistant/components/august/translations/es-419.json +++ b/homeassistant/components/august/translations/es-419.json @@ -9,6 +9,15 @@ "unknown": "Error inesperado" }, "step": { + "reauth_validate": { + "description": "Introduzca la contrase\u00f1a para {username} .", + "title": "Volver a autenticar una cuenta de agosto" + }, + "user_validate": { + "data": { + "login_method": "M\u00e9todo de inicio de sesi\u00f3n" + } + }, "validation": { "data": { "code": "C\u00f3digo de verificaci\u00f3n" diff --git a/homeassistant/components/august/translations/nb.json b/homeassistant/components/august/translations/nb.json new file mode 100644 index 0000000000000..0b5511ab84542 --- /dev/null +++ b/homeassistant/components/august/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user_validate": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/pt-BR.json b/homeassistant/components/august/translations/pt-BR.json index 7186be6216cba..6582c5b60c451 100644 --- a/homeassistant/components/august/translations/pt-BR.json +++ b/homeassistant/components/august/translations/pt-BR.json @@ -1,6 +1,31 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { + "reauth_validate": { + "data": { + "password": "Senha" + }, + "description": "Digite a senha para {username}.", + "title": "Reautenticar uma conta August" + }, + "user_validate": { + "data": { + "login_method": "M\u00e9todo de login", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Se o m\u00e9todo de login for 'e-mail', o nome de usu\u00e1rio ser\u00e1 o endere\u00e7o de e-mail. Se o m\u00e9todo de login for 'telefone', o nome de usu\u00e1rio ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'.", + "title": "Configurar uma conta August" + }, "validation": { "data": { "code": "C\u00f3digo de verifica\u00e7\u00e3o" diff --git a/homeassistant/components/august/translations/sk.json b/homeassistant/components/august/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/august/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 466bf938cb511..54500f5c95aa1 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "codeowners": ["@djtimca"], "requirements": ["auroranoaa==0.0.2"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["auroranoaa"] } diff --git a/homeassistant/components/aurora/translations/el.json b/homeassistant/components/aurora/translations/el.json index e45564b1116c6..c93ed8f34e24f 100644 --- a/homeassistant/components/aurora/translations/el.json +++ b/homeassistant/components/aurora/translations/el.json @@ -1,3 +1,26 @@ { + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u038c\u03c1\u03b9\u03bf (%)" + } + } + } + }, "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 NOAA Aurora" } \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/pt-BR.json b/homeassistant/components/aurora/translations/pt-BR.json new file mode 100644 index 0000000000000..dcaa594cd1314 --- /dev/null +++ b/homeassistant/components/aurora/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Limiar (%)" + } + } + } + }, + "title": "Sensor NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/sk.json b/homeassistant/components/aurora/translations/sk.json new file mode 100644 index 0000000000000..81532ef480193 --- /dev/null +++ b/homeassistant/components/aurora/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py index d9cfb7442314e..5a524851bdfbb 100644 --- a/homeassistant/components/aurora_abb_powerone/aurora_device.py +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -35,8 +35,7 @@ def __init__(self, client: AuroraSerialClient, data: Mapping[str, Any]) -> None: @property def unique_id(self) -> str | None: """Return the unique id for this device.""" - serial = self._data.get(ATTR_SERIAL_NUMBER) - if serial is None: + if (serial := self._data.get(ATTR_SERIAL_NUMBER)) is None: return None return f"{serial}_{self.entity_description.key}" diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 9849c0d84ee7b..d3ab0022a705d 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -7,5 +7,6 @@ "codeowners": [ "@davet2001" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aurorapy"] } diff --git a/homeassistant/components/aurora_abb_powerone/translations/cs.json b/homeassistant/components/aurora_abb_powerone/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/el.json b/homeassistant/components/aurora_abb_powerone/translations/el.json new file mode 100644 index 0000000000000..834c5794861b9 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/el.json @@ -0,0 +1,23 @@ +{ + "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", + "no_serial_ports": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b8\u03cd\u03c1\u03b5\u03c2 com. \u03a7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae RS485 \u03b3\u03b9\u03b1 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1." + }, + "error": { + "cannot_connect": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1, \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7, \u03c4\u03b7\u03bd \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 (\u03c3\u03c4\u03bf \u03c6\u03c9\u03c2 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2)", + "cannot_open_serial_port": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03c4\u03bf \u03ac\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "invalid_serial_port": "\u0397 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03b9", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1", + "port": "\u0398\u03cd\u03c1\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1 RS485 \u03ae USB-RS485" + }, + "description": "\u039f \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03b5\u03bd\u03cc\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1 RS485, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1, \u03cc\u03c0\u03c9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 LCD." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/es-419.json b/homeassistant/components/aurora_abb_powerone/translations/es-419.json new file mode 100644 index 0000000000000..7efa4d7ff4513 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Puerto adaptador RS485 o USB-RS485" + }, + "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485, seleccione el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json b/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json new file mode 100644 index 0000000000000..8fb79bcefa215 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_serial_ports": "Nenhuma porta de comunica\u00e7\u00e3o encontrada. Precisa de um dispositivo RS485 v\u00e1lido para se comunicar." + }, + "error": { + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar, verifique a porta serial, endere\u00e7o, conex\u00e3o el\u00e9trica e se o inversor est\u00e1 ligado (\u00e0 luz do dia)", + "cannot_open_serial_port": "N\u00e3o \u00e9 poss\u00edvel abrir a porta serial, verifique e tente novamente", + "invalid_serial_port": "A porta serial n\u00e3o \u00e9 um dispositivo v\u00e1lido ou n\u00e3o p\u00f4de ser aberta", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "address": "Endere\u00e7o inversor", + "port": "Porta adaptadora RS485 ou USB-RS485" + }, + "description": "O inversor deve ser conectado atrav\u00e9s de um adaptador RS485, selecione a porta serial e o endere\u00e7o do inversor conforme configurado no painel LCD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index d967450788fe8..f3a07616d93ed 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -5,16 +5,17 @@ import logging from aiohttp import ClientError -from aussiebb.asyncio import AussieBB, AuthenticationException +from aussiebb.asyncio import AussieBB +from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_SERVICES, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_ID +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_ID _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] @@ -30,21 +31,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await client.login() - all_services = await client.get_services() + services = await client.get_services() except AuthenticationException as exc: raise ConfigEntryAuthFailed() from exc except ClientError as exc: raise ConfigEntryNotReady() from exc - # Filter the service list to those that are enabled in options - services = [ - s for s in all_services if str(s["service_id"]) in entry.options[CONF_SERVICES] - ] - # Create an appropriate refresh function def update_data_factory(service_id): async def async_update_data(): - return await client.get_usage(service_id) + try: + return await client.get_usage(service_id) + except UnrecognisedServiceType as err: + raise UpdateFailed( + f"Service {service_id} of type '{services[service_id]['type']}' was unrecognised" + ) from err return async_update_data @@ -65,16 +66,10 @@ async def async_update_data(): "services": services, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Reload to update options.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload the config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index c18e808e6ad24..5eaf39853b5a8 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -9,12 +9,10 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -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 .const import CONF_SERVICES, DOMAIN, SERVICE_ID +from .const import CONF_SERVICES, DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -39,11 +37,11 @@ async def async_auth(self, user_input: dict[str, str]) -> dict[str, str] | None: ) try: await self.client.login() - return None except AuthenticationException: return {"base": "invalid_auth"} except ClientError: return {"base": "cannot_connect"} + return None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -61,15 +59,10 @@ async def async_step_user( if not self.services: return self.async_abort(reason="no_services_found") - if len(self.services) == 1: - return self.async_create_entry( - title=self.data[CONF_USERNAME], - data=self.data, - options={CONF_SERVICES: [str(self.services[0][SERVICE_ID])]}, - ) - - # Account has more than one service, select service to add - return await self.async_step_service() + return self.async_create_entry( + title=self.data[CONF_USERNAME], + data=self.data, + ) return self.async_show_form( step_id="user", @@ -82,37 +75,20 @@ async def async_step_user( errors=errors, ) - async def async_step_service( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the optional service selection step.""" - if user_input is not None: - return self.async_create_entry( - title=self.data[CONF_USERNAME], data=self.data, options=user_input - ) + async def async_step_reauth(self, user_input: dict[str, str]) -> FlowResult: + """Handle reauth on credential failure.""" + self._reauth_username = user_input[CONF_USERNAME] - service_options = {str(s[SERVICE_ID]): s["description"] for s in self.services} - return self.async_show_form( - step_id="service", - data_schema=vol.Schema( - { - vol.Required( - CONF_SERVICES, default=list(service_options.keys()) - ): cv.multi_select(service_options) - } - ), - errors=None, - ) + return await self.async_step_reauth_confirm() - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None ) -> FlowResult: - """Handle reauth.""" + """Handle users reauth credentials.""" + errors: dict[str, str] | None = None - if user_input and user_input.get(CONF_USERNAME): - self._reauth_username = user_input[CONF_USERNAME] - elif self._reauth_username and user_input and user_input.get(CONF_PASSWORD): + if user_input and self._reauth_username: data = { CONF_USERNAME: self._reauth_username, CONF_PASSWORD: user_input[CONF_PASSWORD], @@ -130,7 +106,7 @@ async def async_step_reauth( return self.async_create_entry(title=self._reauth_username, data=data) return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", description_placeholders={"username": self._reauth_username}, data_schema=vol.Schema( { @@ -139,46 +115,3 @@ async def async_step_reauth( ), errors=errors, ) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Options flow for picking services.""" - - 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 the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - if self.config_entry.state != config_entries.ConfigEntryState.LOADED: - return self.async_abort(reason="unknown") - data = self.hass.data[DOMAIN][self.config_entry.entry_id] - try: - services = await data["client"].get_services() - except AuthenticationException: - return self.async_abort(reason="invalid_auth") - except ClientError: - return self.async_abort(reason="cannot_connect") - service_options = {str(s[SERVICE_ID]): s["description"] for s in services} - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required( - CONF_SERVICES, - default=self.config_entry.options.get(CONF_SERVICES), - ): cv.multi_select(service_options), - } - ), - ) diff --git a/homeassistant/components/aussie_broadband/diagnostics.py b/homeassistant/components/aussie_broadband/diagnostics.py new file mode 100644 index 0000000000000..f4e95a99f5678 --- /dev/null +++ b/homeassistant/components/aussie_broadband/diagnostics.py @@ -0,0 +1,28 @@ +"""Provides diagnostics for Aussie Broadband.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = ["address", "ipAddresses", "description", "discounts", "coordinator"] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + services = [] + for service in hass.data[DOMAIN][config_entry.entry_id]["services"]: + services.append( + { + "service": async_redact_data(service, TO_REDACT), + "usage": async_redact_data(service["coordinator"].data, ["historical"]), + } + ) + + return {"services": services} diff --git a/homeassistant/components/aussie_broadband/manifest.json b/homeassistant/components/aussie_broadband/manifest.json index fb7ce82832460..5476371f755b6 100644 --- a/homeassistant/components/aussie_broadband/manifest.json +++ b/homeassistant/components/aussie_broadband/manifest.json @@ -4,11 +4,14 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aussie_broadband", "requirements": [ - "pyaussiebb==0.0.9" + "pyaussiebb==0.0.11" ], "codeowners": [ "@nickw444", "@Bre77" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": [ + "aussiebb" + ] } \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index 2ce8aaca9c470..09946cef03d23 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -1,7 +1,9 @@ """Support for Aussie Broadband metric sensors.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, cast from homeassistant.components.sensor import ( SensorEntity, @@ -14,27 +16,36 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, SERVICE_ID -SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + +@dataclass +class SensorValueEntityDescription(SensorEntityDescription): + """Class describing Aussie Broadband sensor entities.""" + + value: Callable = lambda x: x + + +SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Internet Services sensors - SensorEntityDescription( + SensorValueEntityDescription( key="usedMb", name="Data Used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:network", ), - SensorEntityDescription( + SensorValueEntityDescription( key="downloadedMb", name="Downloaded", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download-network", ), - SensorEntityDescription( + SensorValueEntityDescription( key="uploadedMb", name="Uploaded", state_class=SensorStateClass.TOTAL_INCREASING, @@ -42,46 +53,50 @@ icon="mdi:upload-network", ), # Mobile Phone Services sensors - SensorEntityDescription( + SensorValueEntityDescription( key="national", name="National Calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", + value=lambda x: x.get("calls"), ), - SensorEntityDescription( + SensorValueEntityDescription( key="mobile", name="Mobile Calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", + value=lambda x: x.get("calls"), ), - SensorEntityDescription( + SensorValueEntityDescription( key="international", name="International Calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone-plus", ), - SensorEntityDescription( + SensorValueEntityDescription( key="sms", name="SMS Sent", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:message-processing", + value=lambda x: x.get("calls"), ), - SensorEntityDescription( + SensorValueEntityDescription( key="internet", name="Data Used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_KILOBYTES, icon="mdi:network", + value=lambda x: x.get("kbytes"), ), - SensorEntityDescription( + SensorValueEntityDescription( key="voicemail", name="Voicemail Calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", ), - SensorEntityDescription( + SensorValueEntityDescription( key="other", name="Other Calls", entity_registry_enabled_default=False, @@ -89,13 +104,13 @@ icon="mdi:phone", ), # Generic sensors - SensorEntityDescription( + SensorValueEntityDescription( key="daysTotal", name="Billing Cycle Length", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-range", ), - SensorEntityDescription( + SensorValueEntityDescription( key="daysRemaining", name="Billing Cycle Remaining", native_unit_of_measurement=TIME_DAYS, @@ -122,8 +137,10 @@ async def async_setup_entry( class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): """Base class for Aussie Broadband metric sensors.""" + entity_description: SensorValueEntityDescription + def __init__( - self, service: dict[str, Any], description: SensorEntityDescription + self, service: dict[str, Any], description: SensorValueEntityDescription ) -> None: """Initialize the sensor.""" super().__init__(service["coordinator"]) @@ -134,16 +151,13 @@ def __init__( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, service[SERVICE_ID])}, manufacturer="Aussie Broadband", - configuration_url=f"https://my.aussiebroadband.com.au/#/{service['name']}/{service[SERVICE_ID]}/", + configuration_url=f"https://my.aussiebroadband.com.au/#/{service['name'].lower()}/{service[SERVICE_ID]}/", name=service["description"], model=service["name"], ) @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - if self.entity_description.key == "internet": - return self.coordinator.data[self.entity_description.key]["kbytes"] - if self.entity_description.key in ("national", "mobile", "sms"): - return self.coordinator.data[self.entity_description.key]["calls"] - return self.coordinator.data[self.entity_description.key] + parent = self.coordinator.data[self.entity_description.key] + return cast(StateType, self.entity_description.value(parent)) diff --git a/homeassistant/components/aussie_broadband/strings.json b/homeassistant/components/aussie_broadband/strings.json index 42ebcbbdbe823..d5b7d2f1fa1b0 100644 --- a/homeassistant/components/aussie_broadband/strings.json +++ b/homeassistant/components/aussie_broadband/strings.json @@ -13,7 +13,7 @@ "services": "Services" } }, - "reauth": { + "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "Update password for {username}", "data": { diff --git a/homeassistant/components/aussie_broadband/translations/bg.json b/homeassistant/components/aussie_broadband/translations/bg.json index 5f931933f9b72..8c36ef66120fd 100644 --- a/homeassistant/components/aussie_broadband/translations/bg.json +++ b/homeassistant/components/aussie_broadband/translations/bg.json @@ -2,6 +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", + "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" }, "error": { @@ -17,10 +18,18 @@ "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username}", "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": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username}", + "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" + }, "service": { "data": { "services": "\u0423\u0441\u043b\u0443\u0433\u0438" - } + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0423\u0441\u043b\u0443\u0433\u0438" }, "user": { "data": { @@ -40,7 +49,8 @@ "init": { "data": { "services": "\u0423\u0441\u043b\u0443\u0433\u0438" - } + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0423\u0441\u043b\u0443\u0433\u0438" } } } diff --git a/homeassistant/components/aussie_broadband/translations/ca.json b/homeassistant/components/aussie_broadband/translations/ca.json index 6ef46a791018b..83ec53ad5a44c 100644 --- a/homeassistant/components/aussie_broadband/translations/ca.json +++ b/homeassistant/components/aussie_broadband/translations/ca.json @@ -18,6 +18,13 @@ "description": "Actualitza la contrasenya de {username}", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Actualitza la contrasenya de {username}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "service": { "data": { "services": "Serveis" diff --git a/homeassistant/components/aussie_broadband/translations/de.json b/homeassistant/components/aussie_broadband/translations/de.json index 6ab2d4d873aee..ed5cd5fd02c2d 100644 --- a/homeassistant/components/aussie_broadband/translations/de.json +++ b/homeassistant/components/aussie_broadband/translations/de.json @@ -18,6 +18,13 @@ "description": "Passwort f\u00fcr {username} aktualisieren", "title": "Integration erneut authentifizieren" }, + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Passwort f\u00fcr {username} aktualisieren", + "title": "Integration erneut authentifizieren" + }, "service": { "data": { "services": "Dienste" diff --git a/homeassistant/components/aussie_broadband/translations/el.json b/homeassistant/components/aussie_broadband/translations/el.json index 88a9eb36f2f5f..0b78eacb8261e 100644 --- a/homeassistant/components/aussie_broadband/translations/el.json +++ b/homeassistant/components/aussie_broadband/translations/el.json @@ -1,21 +1,50 @@ { "config": { "abort": { - "no_services_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" + "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", + "no_services_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", + "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", + "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": { "reauth": { - "description": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 {username}" + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 {username}", + "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" + }, + "description": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 {username}", + "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" }, "service": { "data": { "services": "\u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd" + }, + "user": { + "data": { + "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" + } } } }, "options": { + "abort": { + "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": { "init": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/en.json b/homeassistant/components/aussie_broadband/translations/en.json index a59a297c2651e..2843916df2e43 100644 --- a/homeassistant/components/aussie_broadband/translations/en.json +++ b/homeassistant/components/aussie_broadband/translations/en.json @@ -18,6 +18,13 @@ "description": "Update password for {username}", "title": "Reauthenticate Integration" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Update password for {username}", + "title": "Reauthenticate Integration" + }, "service": { "data": { "services": "Services" diff --git a/homeassistant/components/aussie_broadband/translations/es-419.json b/homeassistant/components/aussie_broadband/translations/es-419.json new file mode 100644 index 0000000000000..df9322bf01dca --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/es-419.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_services_found": "No se encontraron servicios para esta cuenta" + }, + "step": { + "reauth": { + "description": "Actualizar contrase\u00f1a para {username}" + }, + "service": { + "data": { + "services": "Servicios" + }, + "title": "Seleccione Servicios" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "services": "Servicios" + }, + "title": "Seleccione Servicios" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json new file mode 100644 index 0000000000000..ff13c88c59858 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_services_found": "No se han encontrado servicios para esta cuenta" + }, + "step": { + "reauth": { + "description": "Actualizar la contrase\u00f1a de {username}" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Actualice la contrase\u00f1a para {username}", + "title": "Reautenticar Integraci\u00f3n" + }, + "service": { + "data": { + "services": "Servicios" + }, + "title": "Seleccionar Servicios" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/et.json b/homeassistant/components/aussie_broadband/translations/et.json index 408b4d6adcdc6..7dbf2d26b59fe 100644 --- a/homeassistant/components/aussie_broadband/translations/et.json +++ b/homeassistant/components/aussie_broadband/translations/et.json @@ -18,6 +18,13 @@ "description": "{username} salas\u00f5na v\u00e4rskendamine", "title": "Taastuvasta sidumine" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "{username} salas\u00f5na v\u00e4rskendamine", + "title": "Taastuvasta sidumine" + }, "service": { "data": { "services": "Teenused" diff --git a/homeassistant/components/aussie_broadband/translations/fr.json b/homeassistant/components/aussie_broadband/translations/fr.json index 06b0e44a70a8b..518f05e8ac30c 100644 --- a/homeassistant/components/aussie_broadband/translations/fr.json +++ b/homeassistant/components/aussie_broadband/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_services_found": "Aucun service n'a \u00e9t\u00e9 trouv\u00e9 pour ce compte", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "Impossible de se connecter", @@ -13,7 +15,8 @@ "data": { "password": "Mot de passe" }, - "description": "Mettre \u00e0 jour le mot de passe pour {username}" + "description": "Mettre \u00e0 jour le mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" }, "service": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/he.json b/homeassistant/components/aussie_broadband/translations/he.json index 0a66d494813d1..5861357a6d423 100644 --- a/homeassistant/components/aussie_broadband/translations/he.json +++ b/homeassistant/components/aussie_broadband/translations/he.json @@ -16,6 +16,12 @@ }, "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" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/aussie_broadband/translations/hu.json b/homeassistant/components/aussie_broadband/translations/hu.json index 11e42eaaa0911..a8c3543873d28 100644 --- a/homeassistant/components/aussie_broadband/translations/hu.json +++ b/homeassistant/components/aussie_broadband/translations/hu.json @@ -18,6 +18,13 @@ "description": "Jelsz\u00f3 friss\u00edt\u00e9se {username} sz\u00e1m\u00e1ra", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "Jelsz\u00f3 friss\u00edt\u00e9se {username} sz\u00e1m\u00e1ra", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "service": { "data": { "services": "Szolg\u00e1ltat\u00e1sok" diff --git a/homeassistant/components/aussie_broadband/translations/id.json b/homeassistant/components/aussie_broadband/translations/id.json index ff62d60dafe5f..1802001426872 100644 --- a/homeassistant/components/aussie_broadband/translations/id.json +++ b/homeassistant/components/aussie_broadband/translations/id.json @@ -18,6 +18,13 @@ "description": "Perbarui kata sandi untuk {username}", "title": "Autentikasi Ulang Integrasi" }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, "service": { "data": { "services": "Layanan" diff --git a/homeassistant/components/aussie_broadband/translations/it.json b/homeassistant/components/aussie_broadband/translations/it.json index 3325c33c349f0..a29e10db60a8f 100644 --- a/homeassistant/components/aussie_broadband/translations/it.json +++ b/homeassistant/components/aussie_broadband/translations/it.json @@ -18,6 +18,13 @@ "description": "Aggiorna la password per {username}", "title": "Autentica nuovamente l'integrazione" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Aggiorna la password per {username}", + "title": "Autentica nuovamente l'integrazione" + }, "service": { "data": { "services": "Servizi" diff --git a/homeassistant/components/aussie_broadband/translations/ja.json b/homeassistant/components/aussie_broadband/translations/ja.json index 0fa739c6623e7..f08e02f73c140 100644 --- a/homeassistant/components/aussie_broadband/translations/ja.json +++ b/homeassistant/components/aussie_broadband/translations/ja.json @@ -18,6 +18,13 @@ "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "service": { "data": { "services": "\u30b5\u30fc\u30d3\u30b9" diff --git a/homeassistant/components/aussie_broadband/translations/nb.json b/homeassistant/components/aussie_broadband/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/nl.json b/homeassistant/components/aussie_broadband/translations/nl.json index b7340913f7801..21da52666bc8d 100644 --- a/homeassistant/components/aussie_broadband/translations/nl.json +++ b/homeassistant/components/aussie_broadband/translations/nl.json @@ -1,20 +1,50 @@ { "config": { "abort": { - "no_services_found": "Er zijn geen services gevonden voor dit account" + "already_configured": "Account is al geconfigureerd", + "no_services_found": "Er zijn geen services gevonden voor dit account", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "step": { "reauth": { - "description": "Update wachtwoord voor {username}" + "data": { + "password": "Wachtwoord" + }, + "description": "Update wachtwoord voor {username}", + "title": "Verifieer de integratie opnieuw" + }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Update wachtwoord voor {username}", + "title": "Verifieer de integratie opnieuw" }, "service": { "data": { "services": "Services" + }, + "title": "Selecteer Services" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" } } } }, "options": { + "abort": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/no.json b/homeassistant/components/aussie_broadband/translations/no.json index 193c51f591407..8129a8c9cc261 100644 --- a/homeassistant/components/aussie_broadband/translations/no.json +++ b/homeassistant/components/aussie_broadband/translations/no.json @@ -18,6 +18,13 @@ "description": "Oppdater passordet for {username}", "title": "Godkjenne integrering p\u00e5 nytt" }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Oppdater passordet for {username}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "service": { "data": { "services": "Tjenester" diff --git a/homeassistant/components/aussie_broadband/translations/pl.json b/homeassistant/components/aussie_broadband/translations/pl.json index d17b0f33c2a05..c2f686c11dcc8 100644 --- a/homeassistant/components/aussie_broadband/translations/pl.json +++ b/homeassistant/components/aussie_broadband/translations/pl.json @@ -1,21 +1,50 @@ { "config": { "abort": { - "no_services_found": "Nie znaleziono \u017cadnych us\u0142ug dla tego konta" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "no_services_found": "Nie znaleziono \u017cadnych us\u0142ug dla tego konta", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "reauth": { - "description": "Zaktualizuj has\u0142o dla {username}" + "data": { + "password": "Has\u0142o" + }, + "description": "Zaktualizuj has\u0142o dla {username}", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Zaktualizuj has\u0142o dla {username}", + "title": "Ponownie uwierzytelnij integracj\u0119" }, "service": { "data": { "services": "Us\u0142ugi" }, "title": "Wybierz us\u0142ugi" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } } } }, "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/aussie_broadband/translations/pt-BR.json b/homeassistant/components/aussie_broadband/translations/pt-BR.json new file mode 100644 index 0000000000000..efe1fca7c80d9 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/pt-BR.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "no_services_found": "Nenhum servi\u00e7o foi encontrado para esta conta", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth": { + "data": { + "password": "Senha" + }, + "description": "Atualizar senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Atualizar senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "service": { + "data": { + "services": "Servi\u00e7os" + }, + "title": "Selecionar servi\u00e7os" + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "services": "Servi\u00e7os" + }, + "title": "Selecionar servi\u00e7os" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/ru.json b/homeassistant/components/aussie_broadband/translations/ru.json index ad203bd266e8d..15a9f44a98ac0 100644 --- a/homeassistant/components/aussie_broadband/translations/ru.json +++ b/homeassistant/components/aussie_broadband/translations/ru.json @@ -18,6 +18,13 @@ "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\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" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\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" + }, "service": { "data": { "services": "\u0421\u043b\u0443\u0436\u0431\u044b" diff --git a/homeassistant/components/aussie_broadband/translations/sk.json b/homeassistant/components/aussie_broadband/translations/sk.json new file mode 100644 index 0000000000000..a8cf7db8bbf5e --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + }, + "options": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/sv.json b/homeassistant/components/aussie_broadband/translations/sv.json new file mode 100644 index 0000000000000..a82ed912c19db --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/sv.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "no_services_found": "Inga tj\u00e4nster hittades f\u00f6r detta konto", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautentisera integration" + }, + "service": { + "data": { + "services": "Tj\u00e4nster" + }, + "title": "Valda Tj\u00e4nster" + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "init": { + "data": { + "services": "Tj\u00e4nster" + }, + "title": "Valda Tj\u00e4nster" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/tr.json b/homeassistant/components/aussie_broadband/translations/tr.json index 93c96064d4371..28eae33719db5 100644 --- a/homeassistant/components/aussie_broadband/translations/tr.json +++ b/homeassistant/components/aussie_broadband/translations/tr.json @@ -18,6 +18,13 @@ "description": "{username} i\u00e7in \u015fifreyi g\u00fcncelleyin", "title": "Entegrasyonu Yeniden Do\u011frula" }, + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u015fifreyi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "service": { "data": { "services": "Hizmetler" diff --git a/homeassistant/components/aussie_broadband/translations/zh-Hant.json b/homeassistant/components/aussie_broadband/translations/zh-Hant.json index 282549cdeafcc..f7beefb296275 100644 --- a/homeassistant/components/aussie_broadband/translations/zh-Hant.json +++ b/homeassistant/components/aussie_broadband/translations/zh-Hant.json @@ -18,6 +18,13 @@ "description": "\u66f4\u65b0 {username} \u5bc6\u78bc", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u66f4\u65b0 {username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "service": { "data": { "services": "\u670d\u52d9" diff --git a/homeassistant/components/auth/translations/el.json b/homeassistant/components/auth/translations/el.json index fb65cbae3f800..6b838983637e0 100644 --- a/homeassistant/components/auth/translations/el.json +++ b/homeassistant/components/auth/translations/el.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd." + }, + "error": { + "invalid_code": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + }, + "step": { + "init": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd:", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03af\u03b1\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b4\u03af\u03b4\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2" + }, + "setup": { + "description": "\u0388\u03bd\u03b1\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c4\u03b1\u03bb\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 **notify.{notify_service}**. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03ad \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9:", + "title": "\u0395\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2" + } + }, + "title": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03af\u03b1\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2" + }, "totp": { "error": { "invalid_code": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0395\u03ac\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03ce\u03c2, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03c1\u03bf\u03bb\u03cc\u03b9 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 Home Assistant \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03ba\u03c1\u03b9\u03b2\u03ad\u03c2." diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 901972595e4dd..86fb797ea311b 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -8,11 +8,11 @@ @callback -def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore +def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore[no-untyped-def] """Describe logbook events.""" @callback - def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore + def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore[no-untyped-def] """Describe a logbook event.""" data = event.data message = "has been triggered" diff --git a/homeassistant/components/automation/translations/pt-BR.json b/homeassistant/components/automation/translations/pt-BR.json index 30c78d0a187b6..447658433e5c7 100644 --- a/homeassistant/components/automation/translations/pt-BR.json +++ b/homeassistant/components/automation/translations/pt-BR.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "Desligado", - "on": "Ativa" + "on": "Ligado" } }, "title": "Automa\u00e7\u00e3o" diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 223ceba7685f2..de6581c377264 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/avea", "codeowners": ["@pattyland"], "requirements": ["avea==1.5.1"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["avea"] } diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index c1a3fbd59a761..085f2573a2178 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -5,5 +5,6 @@ "requirements": ["python_awair==0.2.1"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["python_awair"] } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 8225c4088f771..0acefe23c0221 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -1,7 +1,27 @@ { "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", + "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" + }, + "error": { + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { + "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." + }, "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" } } diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 6cec4b5050d65..635a7373b7533 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -1,7 +1,29 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, "error": { - "invalid_access_token": "token de acesso invalido" + "invalid_access_token": "Token de acesso inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token de acesso", + "email": "Email" + }, + "description": "Insira novamente seu token de acesso de desenvolvedor Awair." + }, + "user": { + "data": { + "access_token": "Token de acesso", + "email": "Email" + }, + "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: https://developer.getawair.com/onboard/login" + } } } } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sk.json b/homeassistant/components/awair/translations/sk.json new file mode 100644 index 0000000000000..dabf3e7e9339d --- /dev/null +++ b/homeassistant/components/awair/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "step": { + "reauth": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token", + "email": "Email" + } + }, + "user": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token", + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 761328ba3ded4..41dcb9b2b0b9c 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": ["aiobotocore==2.1.0"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aiobotocore", "botocore"] } diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index bb8b072fb061f..2c9a6a52b9e1e 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -2,20 +2,9 @@ from urllib.parse import urlencode from homeassistant.components.camera import SUPPORT_STREAM -from homeassistant.components.mjpeg.camera import ( - CONF_MJPEG_URL, - CONF_STILL_IMAGE_URL, - MjpegCamera, - filter_urllib3_logging, -) +from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_AUTHENTICATION, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_DIGEST_AUTHENTICATION, -) +from homeassistant.const import HTTP_DIGEST_AUTHENTICATION from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,15 +36,15 @@ def __init__(self, device): """Initialize Axis Communications camera component.""" AxisEntityBase.__init__(self, device) - config = { - CONF_NAME: device.name, - CONF_USERNAME: device.username, - CONF_PASSWORD: device.password, - CONF_MJPEG_URL: self.mjpeg_source, - CONF_STILL_IMAGE_URL: self.image_source, - CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, - } - MjpegCamera.__init__(self, config) + MjpegCamera.__init__( + self, + name=device.name, + username=device.username, + password=device.password, + mjpeg_url=self.mjpeg_source, + still_image_url=self.image_source, + authentication=HTTP_DIGEST_AUTHENTICATION, + ) self._attr_unique_id = f"{device.unique_id}-camera" diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index a2eceff6870f7..3a1017534451c 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -13,6 +13,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -156,7 +157,9 @@ def async_event_callback(self, action, event_id): async_dispatcher_send(self.hass, self.signal_new_event, event_id) @staticmethod - async def async_new_address_callback(hass, entry): + async def async_new_address_callback( + hass: HomeAssistant, entry: ConfigEntry + ) -> None: """Handle signals of device getting new address. Called when config entry is updated. diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 59e723411507d..c07db62a04f87 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==44"], "dhcp": [ + {"registered_devices": true}, { "hostname": "axis-00408c*", "macaddress": "00408C*" @@ -40,5 +41,6 @@ "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"], "quality_scale": "platinum", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["axis"] } diff --git a/homeassistant/components/axis/translations/el.json b/homeassistant/components/axis/translations/el.json index 9bdaac69386df..09ad867132906 100644 --- a/homeassistant/components/axis/translations/el.json +++ b/homeassistant/components/axis/translations/el.json @@ -1,8 +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", + "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", + "not_axis_device": "\u0397 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03b5\u03af\u03c3\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Axis" + }, + "error": { + "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", + "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" + }, "flow_title": "{name} ({host})", "step": { "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Axis" } } diff --git a/homeassistant/components/axis/translations/pt-BR.json b/homeassistant/components/axis/translations/pt-BR.json index 86b6d408baa7f..3208a509ec4e4 100644 --- a/homeassistant/components/axis/translations/pt-BR.json +++ b/homeassistant/components/axis/translations/pt-BR.json @@ -1,19 +1,21 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis" }, "error": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo 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", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "flow_title": "Eixos do dispositivo: {name} ({host})", "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha", "port": "Porta", "username": "Usu\u00e1rio" @@ -21,5 +23,15 @@ "title": "Configurar o dispositivo Axis" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecione o perfil de stream a ser usado" + }, + "title": "Op\u00e7\u00f5es de transmiss\u00e3o de v\u00eddeo do dispositivo Axis" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/sk.json b/homeassistant/components/axis/translations/sk.json new file mode 100644 index 0000000000000..53eb88bf838a4 --- /dev/null +++ b/homeassistant/components/axis/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 213dc19ff9eee..1b1c65ae6d14e 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -123,7 +123,7 @@ def device_info(self) -> DeviceInfo: """Return device information about this Azure DevOps instance.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore + identifiers={(DOMAIN, self._organization, self._project_name)}, # type: ignore[arg-type] manufacturer=self._organization, name=self._project_name, ) diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 1dd0475329396..0500a5856192d 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/azure_devops", "requirements": ["aioazuredevops==1.3.5"], "codeowners": ["@timmo001"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aioazuredevops"] } diff --git a/homeassistant/components/azure_devops/translations/el.json b/homeassistant/components/azure_devops/translations/el.json index bde76bff399e3..55197510f3596 100644 --- a/homeassistant/components/azure_devops/translations/el.json +++ b/homeassistant/components/azure_devops/translations/el.json @@ -1,13 +1,31 @@ { "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", + "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", + "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", + "project_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03ad\u03c1\u03b3\u03bf\u03c5." + }, + "flow_title": "{project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "\u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03a0\u0394\u03a0)" + }, + "description": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03bf {project_url}. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c4\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" }, "user": { "data": { - "organization": "\u039f\u03c1\u03b3\u03ac\u03bd\u03c9\u03c3\u03b7" - } + "organization": "\u039f\u03c1\u03b3\u03ac\u03bd\u03c9\u03c3\u03b7", + "personal_access_token": "\u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03a0\u0394\u03a0)", + "project": "\u0395\u03c1\u03b3\u03bf" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 Azure DevOps \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03ad\u03c1\u03b3\u03bf \u03c3\u03b1\u03c2. \u0388\u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03ad\u03c1\u03b3\u03bf\u03c5 Azure DevOps" } } } diff --git a/homeassistant/components/azure_devops/translations/pt-BR.json b/homeassistant/components/azure_devops/translations/pt-BR.json index 510159829cb20..d8fec75b0f49d 100644 --- a/homeassistant/components/azure_devops/translations/pt-BR.json +++ b/homeassistant/components/azure_devops/translations/pt-BR.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "project_error": "N\u00e3o foi poss\u00edvel obter informa\u00e7\u00f5es do projeto." }, + "flow_title": "{project_url}", "step": { "reauth": { "data": { @@ -16,7 +23,9 @@ "organization": "Organiza\u00e7\u00e3o", "personal_access_token": "Token de acesso pessoal (PAT)", "project": "Projeto" - } + }, + "description": "Configure uma inst\u00e2ncia do Azure DevOps para acessar seu projeto. Um token de acesso pessoal s\u00f3 \u00e9 necess\u00e1rio para um projeto privado.", + "title": "Adicionar o projeto 'Azure DevOps'" } } } diff --git a/homeassistant/components/azure_devops/translations/sk.json b/homeassistant/components/azure_devops/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/client.py b/homeassistant/components/azure_event_hub/client.py index 1a5aa330cc8a2..27a4eabf535c7 100644 --- a/homeassistant/components/azure_event_hub/client.py +++ b/homeassistant/components/azure_event_hub/client.py @@ -64,7 +64,7 @@ def client(self) -> EventHubProducerClient: return EventHubProducerClient( fully_qualified_namespace=f"{self.event_hub_namespace}.servicebus.windows.net", eventhub_name=self.event_hub_instance_name, - credential=EventHubSharedKeyCredential( # type: ignore + credential=EventHubSharedKeyCredential( # type: ignore[arg-type] policy=self.event_hub_sas_policy, key=self.event_hub_sas_key ), **ADDITIONAL_ARGS, diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index 52125b5a79c53..c70b0855b6d80 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -2,8 +2,9 @@ "domain": "azure_event_hub", "name": "Azure Event Hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", - "requirements": ["azure-eventhub==5.5.0"], + "requirements": ["azure-eventhub==5.7.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "cloud_push", - "config_flow": true + "config_flow": true, + "loggers": ["azure"] } diff --git a/homeassistant/components/azure_event_hub/translations/el.json b/homeassistant/components/azure_event_hub/translations/el.json new file mode 100644 index 0000000000000..a68ac09a3fdee --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/el.json @@ -0,0 +1,49 @@ +{ + "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", + "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf yaml \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd.", + "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": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bc\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf yaml \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae config." + }, + "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": { + "conn_string": { + "data": { + "event_hub_connection_string": "\u03a3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Event Hub" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1: {event_hub_instance_name}", + "title": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "sas": { + "data": { + "event_hub_namespace": "\u03a7\u03ce\u03c1\u03bf\u03c2 \u03bf\u03bd\u03bf\u03bc\u03ac\u03c4\u03c9\u03bd Event Hub", + "event_hub_sas_key": "Event Hub SAS Key", + "event_hub_sas_policy": "\u03a0\u03bf\u03bb\u03b9\u03c4\u03b9\u03ba\u03ae \u03c4\u03bf\u03c5 Event Hub SAS" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 SAS (\u03c5\u03c0\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2) \u03b3\u03b9\u03b1: {event_hub_instance_name}", + "title": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 Event Hub", + "use_connection_string": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae\u03c2 \u03c0\u03b1\u03c1\u03c4\u03af\u03b4\u03c9\u03bd \u03c3\u03c4\u03bf\u03bd \u03ba\u03cc\u03bc\u03b2\u03bf." + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/es-419.json b/homeassistant/components/azure_event_hub/translations/es-419.json new file mode 100644 index 0000000000000..0405051cead0f --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/es-419.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "cannot_connect": "No se pudo conectar con las credenciales de configuration.yaml, elim\u00ednelo de yaml y use el flujo de configuraci\u00f3n.", + "unknown": "La conexi\u00f3n con las credenciales de la configuraci\u00f3n.yaml fall\u00f3 con un error desconocido, elim\u00ednelo de yaml y use el flujo de configuraci\u00f3n." + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Cadena de conexi\u00f3n del centro de eventos" + }, + "description": "Ingrese la cadena de conexi\u00f3n para: {event_hub_instance_name}", + "title": "M\u00e9todo de cadena de conexi\u00f3n" + }, + "sas": { + "data": { + "event_hub_namespace": "Espacio de nombres del centro de eventos", + "event_hub_sas_key": "Clave SAS del centro de eventos", + "event_hub_sas_policy": "Pol\u00edtica de SAS del centro de eventos" + }, + "description": "Ingrese las credenciales de SAS (firma de acceso compartido) para: {event_hub_instance_name}", + "title": "M\u00e9todo de credenciales SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nombre de la instancia del centro de eventos", + "use_connection_string": "Usar cadena de conexi\u00f3n" + }, + "title": "Configure su integraci\u00f3n de Azure Event Hub" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervalo entre el env\u00edo de lotes al concentrador." + }, + "title": "Opciones para Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/pt-BR.json b/homeassistant/components/azure_event_hub/translations/pt-BR.json new file mode 100644 index 0000000000000..853a553cd4281 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/pt-BR.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na conex\u00e3o com as credenciais do configuration.yaml. Remova do yaml e use o fluxo de configura\u00e7\u00e3o.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "A conex\u00e3o com as credenciais do configuration.yaml falhou com um erro desconhecido. Remova do yaml e use o fluxo de configura\u00e7\u00e3o." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Cadeia de Conex\u00e3o do Hub de Eventos" + }, + "description": "Insira a string de conex\u00e3o para: {event_hub_instance_name}", + "title": "M\u00e9todo string de conex\u00e3o" + }, + "sas": { + "data": { + "event_hub_namespace": "Namespace do Hub de Eventos", + "event_hub_sas_key": "Chave SAS do Hub de Eventos", + "event_hub_sas_policy": "Pol\u00edtica SAS do Hub de Eventos" + }, + "description": "Insira as credenciais SAS (assinatura de acesso compartilhado) para: {event_hub_instance_name}", + "title": "M\u00e9todo de credenciais SAS" + }, + "user": { + "data": { + "event_hub_instance_name": "Nome da inst\u00e2ncia do hub de eventos", + "use_connection_string": "Use string de conex\u00e3o" + }, + "title": "Configure sua integra\u00e7\u00e3o do Hub de Eventos do Azure" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervalo entre o envio de comandos para o hub." + }, + "title": "Op\u00e7\u00f5es para o Hub de Eventos do Azure." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/zh-Hant.json b/homeassistant/components/azure_event_hub/translations/zh-Hant.json index 64f713f5bd84b..a963a7ab4861b 100644 --- a/homeassistant/components/azure_event_hub/translations/zh-Hant.json +++ b/homeassistant/components/azure_event_hub/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u4f7f\u7528 configuration.yaml \u6240\u5305\u542b\u6191\u8b49\u9023\u7dda\u5931\u6557\uff0c\u8acb\u81ea Yaml \u79fb\u9664\u8a72\u6191\u8b49\u4e26\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u65b9\u5f0f\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u4f7f\u7528 configuration.yaml \u6240\u5305\u542b\u6191\u8b49\u9023\u7dda\u767c\u751f\u672a\u77e5\u932f\u8aa4\uff0c\u8acb\u81ea Yaml \u79fb\u9664\u8a72\u6191\u8b49\u4e26\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u65b9\u5f0f\u3002" }, "error": { diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 5de15056b0874..6cf5e2bf406ab 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", "requirements": ["azure-servicebus==0.50.3"], "codeowners": ["@hfurubotten"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["azure"] } diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index e808da427281e..446551ec3a111 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": ["baidu-aip==1.6.6"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aip"] } diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 4145ea8d80712..81016bbeb3396 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -99,8 +99,7 @@ def hvac_mode(self) -> str: @property def hvac_action(self) -> str: """Return the current operation mode.""" - state = self._client.get_heatstate() - if state >= self._client.ON: + if self._client.get_heatstate() >= self._client.ON: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE diff --git a/homeassistant/components/balboa/manifest.json b/homeassistant/components/balboa/manifest.json index aa52bee230d7f..d6ef4094b07f5 100644 --- a/homeassistant/components/balboa/manifest.json +++ b/homeassistant/components/balboa/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@garbled1" ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pybalboa"] } diff --git a/homeassistant/components/balboa/translations/cs.json b/homeassistant/components/balboa/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/balboa/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/el.json b/homeassistant/components/balboa/translations/el.json index 63210112cbb2a..e85920108d3f2 100644 --- a/homeassistant/components/balboa/translations/el.json +++ b/homeassistant/components/balboa/translations/el.json @@ -1,7 +1,17 @@ { "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" + }, "step": { "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Balboa Wi-Fi" } } diff --git a/homeassistant/components/balboa/translations/es-419.json b/homeassistant/components/balboa/translations/es-419.json new file mode 100644 index 0000000000000..16aa28fb6b2e5 --- /dev/null +++ b/homeassistant/components/balboa/translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "title": "Con\u00e9ctese al dispositivo Wi-Fi de Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Mant\u00e9n sincronizada la hora de tu Cliente Balboa Spa con Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pt-BR.json b/homeassistant/components/balboa/translations/pt-BR.json new file mode 100644 index 0000000000000..cd16bb3cec959 --- /dev/null +++ b/homeassistant/components/balboa/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + }, + "title": "Conecte-se ao dispositivo Wi-Fi Balboa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "Mantenha o hor\u00e1rio do seu cliente Balboa Spa sincronizado com o Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index add067ab0ccfb..c57530a9bf856 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": ["Adafruit_BBIO==1.1.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["Adafruit_BBIO"] } diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index a59023bb3f524..4f298b2b5e9ae 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": ["pybbox==0.0.5-alpha"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pybbox"] } diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 941faf1b598c0..b334ab36b36f9 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": ["beewi_smartclim==0.0.10"], "codeowners": ["@alemuro"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["beewi_smartclim"] } diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index f784b029a01d8..807f7a9e05f1a 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["i2csense", "smbus"] } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 62a7303623817..62ff81877311f 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -115,9 +115,9 @@ "off": "Not charging", "on": "Charging" }, - "co": { - "off": "Clear", - "on": "Detected" + "carbon_monoxide": { + "off": "[%key:component::binary_sensor::state::gas::off%]", + "on": "[%key:component::binary_sensor::state::gas::on%]" }, "cold": { "off": "[%key:component::binary_sensor::state::battery::off%]", diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index e2450646ef854..4c27c1e2966ff 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -134,6 +134,10 @@ "off": "No carregant", "on": "Carregant" }, + "carbon_monoxide": { + "off": "Lliure", + "on": "Detectat" + }, "co": { "off": "Lliure", "on": "Detectat" diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index d3f2bd7fb7fc5..1d6257c48c6a0 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -134,6 +134,10 @@ "off": "L\u00e4dt nicht", "on": "L\u00e4dt" }, + "carbon_monoxide": { + "off": "Normal", + "on": "Erkannt" + }, "co": { "off": "Normal", "on": "Erkannt" diff --git a/homeassistant/components/binary_sensor/translations/el.json b/homeassistant/components/binary_sensor/translations/el.json index 63469b85c0cb1..1cfe13d694b1b 100644 --- a/homeassistant/components/binary_sensor/translations/el.json +++ b/homeassistant/components/binary_sensor/translations/el.json @@ -1,12 +1,18 @@ { "device_automation": { "condition_type": { + "is_bat_low": "\u0397 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 {entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c7\u03b1\u03bc\u03b7\u03bb\u03ae", "is_co": "\u039f {entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1", + "is_cold": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03cd\u03bf", + "is_connected": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf", + "is_gas": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b1\u03ad\u03c1\u03b9\u03bf", + "is_hot": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b6\u03b5\u03c3\u03c4\u03cc", "is_light": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c6\u03c9\u03c2", "is_locked": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf", "is_moist": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c5\u03b3\u03c1\u03cc", "is_motion": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7", "is_moving": "{entity_name} \u03ba\u03b9\u03bd\u03b5\u03af\u03c4\u03b1\u03b9", + "is_no_co": "{entity_name} \u03b4\u03b5\u03bd \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1", "is_no_gas": "{entity_name} \u03b4\u03b5\u03bd \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b1\u03ad\u03c1\u03b9\u03bf", "is_no_light": "{entity_name} \u03b4\u03b5\u03bd \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c6\u03c9\u03c2", "is_no_motion": "{entity_name} \u03b4\u03b5\u03bd \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7", @@ -24,8 +30,11 @@ "is_not_moving": "{entity_name} \u03b4\u03b5\u03bd \u03ba\u03b9\u03bd\u03b5\u03af\u03c4\u03b1\u03b9", "is_not_occupied": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03c4\u03b5\u03b9\u03bb\u03b7\u03bc\u03bc\u03ad\u03bd\u03bf", "is_not_open": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "is_not_plugged_in": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf", "is_not_powered": "{entity_name} \u03b4\u03b5\u03bd \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af\u03c4\u03b1\u03b9", "is_not_present": "{entity_name} \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9", + "is_not_running": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9", + "is_not_tampered": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "is_not_unsafe": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2", "is_occupied": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03c4\u03b5\u03b9\u03bb\u03b7\u03bc\u03bc\u03ad\u03bd\u03bf", "is_off": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", @@ -35,23 +44,67 @@ "is_powered": "{entity_name} \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af\u03c4\u03b1\u03b9", "is_present": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b1\u03c1\u03cc\u03bd", "is_problem": "{entity_name} \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1", + "is_running": "{entity_name} \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9", "is_smoke": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03b1\u03c0\u03bd\u03cc", "is_sound": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ae\u03c7\u03bf", + "is_tampered": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "is_unsafe": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2", "is_update": "{entity_name} \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7", "is_vibration": "{entity_name} \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b4\u03cc\u03bd\u03b7\u03c3\u03b7" }, "trigger_type": { "bat_low": "{entity_name} \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c7\u03b1\u03bc\u03b7\u03bb\u03ae", + "co": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1", "cold": "{entity_name} \u03ba\u03c1\u03cd\u03c9\u03c3\u03b5", "connected": "{entity_name} \u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5", + "gas": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b1\u03ad\u03c1\u03b9\u03bf", "hot": "{entity_name} \u03b6\u03b5\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", + "is_not_tampered": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "is_tampered": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "light": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c6\u03c9\u03c2", "locked": "{entity_name} \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03b8\u03b7\u03ba\u03b5", "moist": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03c5\u03b3\u03c1\u03cc", + "motion": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "moving": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03ba\u03b9\u03bd\u03b5\u03af\u03c4\u03b1\u03b9", + "no_co": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1", + "no_gas": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b1\u03ad\u03c1\u03b9\u03bf", + "no_light": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c6\u03c9\u03c2", + "no_motion": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "no_problem": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1", + "no_smoke": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03b1\u03c0\u03bd\u03cc", + "no_sound": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ae\u03c7\u03bf", "no_update": "{entity_name} \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5", + "no_vibration": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b4\u03cc\u03bd\u03b7\u03c3\u03b7", + "not_bat_low": "\u0397 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ae", + "not_cold": "{entity_name} \u03bc\u03b5\u03c4\u03b5\u03c4\u03c1\u03ac\u03c0\u03b7 \u03c3\u03b5 \u03bc\u03b7 \u03ba\u03c1\u03cd\u03bf", + "not_connected": "{entity_name} \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5", + "not_hot": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03bc\u03b7 \u03ba\u03b1\u03c5\u03c4\u03cc", + "not_locked": "{entity_name} \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03b8\u03b7\u03ba\u03b5", + "not_moist": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03be\u03b7\u03c1\u03cc", + "not_moving": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03ba\u03b9\u03bd\u03b5\u03af\u03c4\u03b1\u03b9", + "not_occupied": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03bc\u03b7 \u03ba\u03b1\u03c4\u03b5\u03b9\u03bb\u03b7\u03bc\u03bc\u03ad\u03bd\u03bf", "not_opened": "{entity_name} \u03ad\u03ba\u03bb\u03b5\u03b9\u03c3\u03b5", - "update": "{entity_name} \u03ad\u03bb\u03b1\u03b2\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" + "not_plugged_in": "{entity_name} \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf", + "not_powered": "{entity_name} \u03b4\u03b5\u03bd \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af\u03c4\u03b1\u03b9", + "not_present": "{entity_name} \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9", + "not_running": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd", + "not_tampered": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "not_unsafe": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2", + "occupied": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03ba\u03b1\u03c4\u03b5\u03b9\u03bb\u03b7\u03bc\u03bc\u03ad\u03bd\u03bf", + "opened": "{entity_name} \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", + "plugged_in": "{entity_name} \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf", + "powered": "{entity_name} \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf", + "present": "{entity_name} \u03c0\u03b1\u03c1\u03cc\u03bd", + "problem": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1", + "running": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9", + "smoke": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ba\u03b1\u03c0\u03bd\u03cc", + "sound": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03ae\u03c7\u03bf", + "tampered": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", + "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", + "unsafe": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2", + "update": "{entity_name} \u03ad\u03bb\u03b1\u03b2\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7", + "vibration": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b4\u03cc\u03bd\u03b7\u03c3\u03b7" } }, "device_class": { @@ -81,6 +134,10 @@ "off": "\u0394\u03b5 \u03c6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", "on": "\u03a6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9" }, + "carbon_monoxide": { + "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" + }, "co": { "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" @@ -102,7 +159,7 @@ "on": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1" }, "gas": { - "off": "\u0394\u03b5\u03bd \u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", + "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" }, "heat": { @@ -149,6 +206,10 @@ "off": "\u0395\u03bd\u03c4\u03ac\u03be\u03b5\u03b9", "on": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1" }, + "running": { + "off": "\u0394\u03b5\u03bd \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9", + "on": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03b9\u03c4\u03b1\u03b9" + }, "safety": { "off": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2", "on": "\u0391\u03bd\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index fd69ab678a784..1d4f30fef52d2 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -134,6 +134,10 @@ "off": "Not charging", "on": "Charging" }, + "carbon_monoxide": { + "off": "Clear", + "on": "Detected" + }, "co": { "off": "Clear", "on": "Detected" diff --git a/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant/components/binary_sensor/translations/es-419.json index dad07f9b771f2..2951e71b114ed 100644 --- a/homeassistant/components/binary_sensor/translations/es-419.json +++ b/homeassistant/components/binary_sensor/translations/es-419.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_co": "{entity_name} est\u00e1 detectando mon\u00f3xido de carbono", "is_cold": "{entity_name} est\u00e1 fr\u00edo", "is_connected": "{entity_name} est\u00e1 conectado", "is_gas": "{entity_name} est\u00e1 detectando gas", @@ -11,12 +12,14 @@ "is_moist": "{entity_name} est\u00e1 h\u00famedo", "is_motion": "{entity_name} est\u00e1 detectando movimiento", "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_co": "{entity_name} no detecta mon\u00f3xido de carbono", "is_no_gas": "{entity_name} no detecta gas", "is_no_light": "{entity_name} no detecta luz", "is_no_motion": "{entity_name} no detecta movimiento", "is_no_problem": "{entity_name} no detecta el problema", "is_no_smoke": "{entity_name} no detecta humo", "is_no_sound": "{entity_name} no detecta sonido", + "is_no_update": "{entity_name} est\u00e1 actualizado", "is_no_vibration": "{entity_name} no detecta vibraciones", "is_not_bat_low": "{entity_name} bater\u00eda est\u00e1 normal", "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", @@ -30,6 +33,8 @@ "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", "is_not_powered": "{entity_name} no tiene encendido", "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_running": "{entity_name} no se est\u00e1 ejecutando", + "is_not_tampered": "{entity_name} no detecta la manipulaci\u00f3n", "is_not_unsafe": "{entity_name} es seguro", "is_occupied": "{entity_name} est\u00e1 ocupado", "is_off": "{entity_name} est\u00e1 apagado", @@ -39,6 +44,7 @@ "is_powered": "{entity_name} est\u00e1 encendido", "is_present": "{entity_name} est\u00e1 presente", "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_running": "{entity_name} se est\u00e1 ejecutando", "is_smoke": "{entity_name} est\u00e1 detectando humo", "is_sound": "{entity_name} est\u00e1 detectando sonido", "is_unsafe": "{entity_name} es inseguro", @@ -89,6 +95,19 @@ "vibration": "{entity_name} comenz\u00f3 a detectar vibraciones" } }, + "device_class": { + "cold": "fr\u00edo", + "gas": "gas", + "heat": "calor", + "moisture": "humedad", + "motion": "movimiento", + "occupancy": "ocupaci\u00f3n", + "power": "energ\u00eda", + "problem": "problema", + "smoke": "humo", + "sound": "sonido", + "vibration": "vibraci\u00f3n" + }, "state": { "_": { "off": "Desactivado", @@ -102,6 +121,10 @@ "off": "No esta cargando", "on": "Cargando" }, + "co": { + "off": "Despejado", + "on": "Detectado" + }, "cold": { "off": "Normal", "on": "Fr\u00edo" @@ -166,6 +189,10 @@ "off": "OK", "on": "Problema" }, + "running": { + "off": "No se est\u00e1 ejecutando", + "on": "Corriendo" + }, "safety": { "off": "Seguro", "on": "Inseguro" @@ -178,6 +205,10 @@ "off": "Despejado", "on": "Detectado" }, + "update": { + "off": "Actualizado", + "on": "Actualizaci\u00f3n disponible" + }, "vibration": { "off": "Despejado", "on": "Detectado" diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 241784aeea00e..92b4e52952d8a 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -134,6 +134,10 @@ "off": "Ei lae", "on": "Laeb" }, + "carbon_monoxide": { + "off": "Korras", + "on": "Tuvastatud" + }, "co": { "off": "Puudub", "on": "Tuvastatud" diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index d95b02d3c8b01..3690c1def1eff 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -134,6 +134,10 @@ "off": "Nem t\u00f6lt\u0151dik", "on": "T\u00f6lt\u0151dik" }, + "carbon_monoxide": { + "off": "Norm\u00e1l", + "on": "\u00c9szlelve" + }, "co": { "off": "Tiszta", "on": "\u00c9rz\u00e9kelve" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index efe7d6da3a7d6..5c81e8942e469 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -134,6 +134,10 @@ "off": "Non in carica", "on": "In carica" }, + "carbon_monoxide": { + "off": "Assente", + "on": "Rilevato" + }, "co": { "off": "Non Rilevato", "on": "Rilevato" diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 3d961086dfc67..541c507396136 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -134,6 +134,10 @@ "off": "\u5145\u96fb\u3057\u3066\u3044\u306a\u3044", "on": "\u5145\u96fb" }, + "carbon_monoxide": { + "off": "\u30af\u30ea\u30a2", + "on": "\u691c\u51fa" + }, "co": { "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa\u3055\u308c\u307e\u3057\u305f" @@ -144,7 +148,7 @@ }, "connectivity": { "off": "\u5207\u65ad", - "on": "\u63a5\u7d9a\u6e08" + "on": "\u63a5\u7d9a\u6e08\u307f" }, "door": { "off": "\u9589\u9396", diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 504b9a4dd48df..7afbcaf616f7f 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -134,6 +134,10 @@ "off": "Niet aan het opladen", "on": "Opladen" }, + "carbon_monoxide": { + "off": "Niet gedetecteerd", + "on": "Gedetecteerd" + }, "co": { "off": "Niet gedetecteerd", "on": "Gedetecteerd" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index a014b252144be..62cf2d7cc1b50 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -134,6 +134,10 @@ "off": "Lader ikke", "on": "Lader" }, + "carbon_monoxide": { + "off": "Klart", + "on": "Oppdaget" + }, "co": { "off": "Klart", "on": "Oppdaget" diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 55e2dee35cd9f..d318968da5663 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -134,6 +134,10 @@ "off": "roz\u0142adowywanie", "on": "\u0142adowanie" }, + "carbon_monoxide": { + "off": "brak", + "on": "wykryto" + }, "co": { "off": "brak", "on": "wykryto" diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index d1bf94170bf86..779830e0961bc 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -1,12 +1,110 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} a bateria est\u00e1 fraca", + "is_co": "{entity_name} est\u00e1 detectando mon\u00f3xido de carbono", + "is_cold": "{entity_name} \u00e9 frio", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando g\u00e1s", + "is_hot": "{entity_name} \u00e9 quente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 \u00famido", "is_motion": "{entity_name} est\u00e1 detectando movimento", - "is_no_motion": "{entity_name} n\u00e3o est\u00e1 detectando movimento" + "is_moving": "{entity_name} est\u00e1 se movendo", + "is_no_co": "{entity_name} n\u00e3o est\u00e1 detectando mon\u00f3xido de carbono", + "is_no_gas": "{entity_name} n\u00e3o est\u00e1 detectando g\u00e1s", + "is_no_light": "{entity_name} n\u00e3o est\u00e1 detectando luz", + "is_no_motion": "{entity_name} n\u00e3o est\u00e1 detectando movimento", + "is_no_problem": "{entity_name} n\u00e3o est\u00e1 detectando problema", + "is_no_smoke": "{entity_name} n\u00e3o est\u00e1 detectando fuma\u00e7a", + "is_no_sound": "{entity_name} n\u00e3o est\u00e1 detectando som", + "is_no_update": "{entity_name} est\u00e1 atualizado", + "is_no_vibration": "{entity_name} n\u00e3o est\u00e1 detectando vibra\u00e7\u00e3o", + "is_not_bat_low": "{entity_name} bateria normal", + "is_not_cold": "{entity_name} n\u00e3o \u00e9 frio", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} n\u00e3o \u00e9 quente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} est\u00e1 parado", + "is_not_occupied": "{entity_name} n\u00e3o est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 fechado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} n\u00e3o \u00e9 alimentado", + "is_not_present": "{entity_name} n\u00e3o est\u00e1 presente", + "is_not_running": "{entity_name} n\u00e3o est\u00e1 em execu\u00e7\u00e3o", + "is_not_tampered": "{entity_name} n\u00e3o est\u00e1 detectando adultera\u00e7\u00e3o", + "is_not_unsafe": "{entity_name} \u00e9 seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_open": "{entity_name} est\u00e1 aberto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} \u00e9 alimentado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando problema", + "is_running": "{entity_name} est\u00e1 em execu\u00e7\u00e3o", + "is_smoke": "{entity_name} est\u00e1 detectando fuma\u00e7a", + "is_sound": "{entity_name} est\u00e1 detectando som", + "is_tampered": "{entity_name} est\u00e1 detectando adultera\u00e7\u00e3o", + "is_unsafe": "{entity_name} \u00e9 inseguro", + "is_update": "{entity_name} tem uma atualiza\u00e7\u00e3o dispon\u00edvel", + "is_vibration": "{entity_name} est\u00e1 detectando vibra\u00e7\u00e3o" }, "trigger_type": { + "bat_low": "{entity_name} bateria fraca", + "co": "{entity_name} come\u00e7ou a detectar mon\u00f3xido de carbono", + "cold": "{entity_name} frio", + "connected": "{entity_name} conectado", + "gas": "{entity_name} come\u00e7ou a detectar g\u00e1s", + "hot": "{entity_name} tornou-se quente", + "is_not_tampered": "{entity_name} parar de detectar adultera\u00e7\u00e3o", + "is_tampered": "{entity_name} come\u00e7ar a detectar adultera\u00e7\u00e3o", + "light": "{entity_name} come\u00e7ou a detectar luz", + "locked": "{entity_name} bloqueado", + "moist": "{entity_name} ficar \u00famido", "motion": "{entity_name} come\u00e7ou a detectar movimento", - "no_motion": "{entity_name} parou de detectar movimento" + "moving": "{entity_name} come\u00e7ou a se mover", + "no_co": "{entity_name} parou de detectar mon\u00f3xido de carbono", + "no_gas": "{entity_name} parou de detectar g\u00e1s", + "no_light": "{entity_name} parou de detectar luz", + "no_motion": "{entity_name} parou de detectar movimento", + "no_problem": "{entity_name} parou de detectar problema", + "no_smoke": "{entity_name} parou de detectar fuma\u00e7a", + "no_sound": "{entity_name} parou de detectar som", + "no_update": "{entity_name} for atualizado", + "no_vibration": "{entity_name} parou de detectar vibra\u00e7\u00e3o", + "not_bat_low": "{entity_name} bateria normal", + "not_cold": "{entity_name} n\u00e3o frio", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} n\u00e3o quente", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} secou", + "not_moving": "{entity_name} parado", + "not_occupied": "{entity_name} desocupado", + "not_opened": "{entity_name} for fechado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} sem alimenta\u00e7\u00e3o", + "not_present": "{entity_name} n\u00e3o est\u00e1 presente", + "not_running": "{entity_name} n\u00e3o estiver mais em execu\u00e7\u00e3o", + "not_tampered": "{entity_name} parou de detectar adultera\u00e7\u00e3o", + "not_unsafe": "{entity_name} seguro", + "occupied": "{entity_name} ocupado", + "opened": "{entity_name} aberto", + "plugged_in": "{entity_name} conectado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "{entity_name} come\u00e7ou a detectar problema", + "running": "{entity_name} come\u00e7ar a correr", + "smoke": "{entity_name} come\u00e7ou a detectar fuma\u00e7a", + "sound": "{entity_name} come\u00e7ou a detectar som", + "tampered": "{entity_name} come\u00e7ou a detectar adultera\u00e7\u00e3o", + "turned_off": "{entity_name} desligado", + "turned_on": "{entity_name} ligado", + "unsafe": "{entity_name} tornou-se inseguro", + "update": "{entity_name} tiver uma atualiza\u00e7\u00e3o dispon\u00edvel", + "vibration": "{entity_name} come\u00e7ou a detectar vibra\u00e7\u00e3o" } }, "device_class": { @@ -36,7 +134,12 @@ "off": "N\u00e3o est\u00e1 carregando", "on": "Carregando" }, + "carbon_monoxide": { + "off": "Remover", + "on": "Detectou" + }, "co": { + "off": "Limpo", "on": "Detectado" }, "cold": { @@ -56,7 +159,7 @@ "on": "Aberto" }, "gas": { - "off": "Limpo", + "off": "Normal", "on": "Detectado" }, "heat": { @@ -69,14 +172,14 @@ }, "lock": { "off": "Trancado", - "on": "Desbloqueado" + "on": "Destrancado" }, "moisture": { "off": "Seco", "on": "Molhado" }, "motion": { - "off": "Desligado", + "off": "Sem movimento", "on": "Detectado" }, "moving": { diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index 45c73269a720f..cc9573e2b14f9 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -134,6 +134,10 @@ "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0436\u0430\u0435\u0442\u0441\u044f", "on": "\u0417\u0430\u0440\u044f\u0436\u0430\u0435\u0442\u0441\u044f" }, + "carbon_monoxide": { + "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", + "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" + }, "co": { "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json index 0f8d92749c4cb..c423002359ee4 100644 --- a/homeassistant/components/binary_sensor/translations/uk.json +++ b/homeassistant/components/binary_sensor/translations/uk.json @@ -187,5 +187,5 @@ "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" } }, - "title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a" + "title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440" } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index 4c14fb93fe737..a0adaaab083f9 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -134,6 +134,10 @@ "off": "\u672a\u5728\u5145\u96fb", "on": "\u5145\u96fb\u4e2d" }, + "carbon_monoxide": { + "off": "\u672a\u89f8\u767c", + "on": "\u5df2\u89f8\u767c" + }, "co": { "off": "\u672a\u5075\u6e2c", "on": "\u5075\u6e2c" diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index 0a8abfa6500b4..2cd9453f4b831 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": ["blockchain==1.4.4"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["blockchain"] } diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index c8923f3d541fe..c18bd8b5de213 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "codeowners": ["@UgaitzEtxebarria"], "requirements": ["bizkaibus==0.1.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["bizkaibus"] } diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index 04bde4b4617fe..44645397c2dd6 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": ["pyblackbird==0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyblackbird"] } diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 39c0d37e2e3c3..d9c0481fff65c 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/blebox", "requirements": ["blebox_uniapi==1.3.3"], "codeowners": ["@bbx-a", "@bbx-jp"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["blebox_uniapi"] } diff --git a/homeassistant/components/blebox/translations/el.json b/homeassistant/components/blebox/translations/el.json index 85d796ff6ddac..c6c2f597f1d28 100644 --- a/homeassistant/components/blebox/translations/el.json +++ b/homeassistant/components/blebox/translations/el.json @@ -1,14 +1,21 @@ { "config": { "abort": { - "address_already_configured": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae BleBox \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {address}." + "address_already_configured": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae BleBox \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {address}.", + "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", "unsupported_version": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae BleBox \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b1\u03bb\u03b9\u03cc \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03b1." }, "flow_title": "{name} ({host})", "step": { "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "port": "\u0398\u03cd\u03c1\u03b1" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf BleBox \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 BleBox" } diff --git a/homeassistant/components/blebox/translations/pt-BR.json b/homeassistant/components/blebox/translations/pt-BR.json index 972aed55cc493..5a67276761702 100644 --- a/homeassistant/components/blebox/translations/pt-BR.json +++ b/homeassistant/components/blebox/translations/pt-BR.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "address_already_configured": "Um dispositivo BleBox j\u00e1 est\u00e1 configurado em {address}.", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado", + "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Por favor, atualize-o primeiro." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "Endere\u00e7o IP", "port": "Porta" - } + }, + "description": "Configure seu BleBox para integrar com o Home Assistant.", + "title": "Configure seu dispositivo BleBox" } } } diff --git a/homeassistant/components/blebox/translations/sk.json b/homeassistant/components/blebox/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/blebox/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index cc185061ee996..58e2d3a1b5227 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -12,8 +12,13 @@ { "hostname": "blink*", "macaddress": "00037F*" - } + }, + { + "hostname": "blink*", + "macaddress": "20A171*" + } ], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["blinkpy"] } diff --git a/homeassistant/components/blink/translations/el.json b/homeassistant/components/blink/translations/el.json index b6981eeb66adf..690d2bf053966 100644 --- a/homeassistant/components/blink/translations/el.json +++ b/homeassistant/components/blink/translations/el.json @@ -1,5 +1,14 @@ { "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_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\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": { "2fa": { "data": { @@ -9,8 +18,23 @@ "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, "user": { + "data": { + "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" + }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Blink" } } + }, + "options": { + "step": { + "simple_options": { + "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": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Blink", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Blink" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pt-BR.json b/homeassistant/components/blink/translations/pt-BR.json index 70d8b8620c45d..440558cddd7b4 100644 --- a/homeassistant/components/blink/translations/pt-BR.json +++ b/homeassistant/components/blink/translations/pt-BR.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { + "cannot_connect": "Falha ao conectar", + "invalid_access_token": "Token de acesso inv\u00e1lido", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -12,16 +14,27 @@ "data": { "2fa": "C\u00f3digo de dois fatores" }, - "description": "Digite o pin enviado para o seu e-mail. Se o e-mail n\u00e3o contiver um pin, deixe em branco", + "description": "Digite o PIN enviado para o seu e-mail", "title": "Autentica\u00e7\u00e3o de dois fatores" }, "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "title": "Entrar com a conta Blink" } } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Intervalo de varredura (segundos)" + }, + "description": "Configurar integra\u00e7\u00e3o Blink", + "title": "Op\u00e7\u00f5es Blink" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/blink/translations/sk.json b/homeassistant/components/blink/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/blink/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index 05f8fe65fb3c7..b7058494e5cc0 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": ["blinkstick==1.2.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["blinkstick"] } diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index c7c37c9bd0dd8..712f90a0f26a6 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": ["python-blockchain-api==0.0.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyblockchain"] } diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 564aef45f8442..7552c024d6219 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": ["pygatt[GATTTOOL]==4.0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pygatt"] } diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index ccf48a9b8c37e..ad8ee782592c7 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": ["bt_proximity==0.2.1", "pybluez==0.22"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bluetooth", "bt_proximity"] } diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 4c997152b5a87..8a283b40f5fab 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -8,5 +8,6 @@ "bme280spi==0.2.0" ], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["bme280spi", "i2csense", "smbus"] } diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index 16e841b942f5c..c4db1d640de92 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["bme680", "smbus"] } diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 9698679a6d628..ef5a376034a42 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,8 +2,9 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.8.10"], + "requirements": ["bimmer_connected==0.8.11"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["bimmer_connected"] } diff --git a/homeassistant/components/bmw_connected_drive/translations/el.json b/homeassistant/components/bmw_connected_drive/translations/el.json new file mode 100644 index 0000000000000..bb20b0a61a26b --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/el.json @@ -0,0 +1,30 @@ +{ + "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" + }, + "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" + }, + "step": { + "user": { + "data": { + "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 ConnectedDrive", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u039c\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 (\u03bc\u03cc\u03bd\u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2, \u03cc\u03c7\u03b9 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd, \u03cc\u03c7\u03b9 \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1)", + "use_location": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03bf\u03c0\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03c5\u03c4\u03bf\u03ba\u03b9\u03bd\u03ae\u03c4\u03bf\u03c5 (\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 i3/i8 \u03ba\u03b1\u03b9 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03b1\u03c1\u03b1\u03c7\u03b8\u03b5\u03af \u03c0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/es-419.json b/homeassistant/components/bmw_connected_drive/translations/es-419.json index 0bce46abd9709..bb19124b55c9c 100644 --- a/homeassistant/components/bmw_connected_drive/translations/es-419.json +++ b/homeassistant/components/bmw_connected_drive/translations/es-419.json @@ -12,7 +12,8 @@ "step": { "account_options": { "data": { - "read_only": "Solo lectura (solo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)" + "read_only": "Solo lectura (solo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)", + "use_location": "Use la ubicaci\u00f3n de Home Assistant para encuestas de ubicaci\u00f3n de autom\u00f3viles (obligatorio para veh\u00edculos que no sean i3/i8 fabricados antes de julio de 2014)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/id.json b/homeassistant/components/bmw_connected_drive/translations/id.json index e49e9202dbe2e..3701a49ccfbef 100644 --- a/homeassistant/components/bmw_connected_drive/translations/id.json +++ b/homeassistant/components/bmw_connected_drive/translations/id.json @@ -22,7 +22,7 @@ "account_options": { "data": { "read_only": "Hanya baca (hanya sensor dan notifikasi, tidak ada eksekusi layanan, tidak ada fitur penguncian)", - "use_location": "Gunakan lokasi Asisten Rumah untuk polling lokasi mobil (diperlukan untuk kendaraan non i3/i8 yang diproduksi sebelum Juli 2014)" + "use_location": "Gunakan lokasi Home Assistant untuk polling lokasi mobil (diperlukan untuk kendaraan non i3/i8 yang diproduksi sebelum Juli 2014)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/pt-BR.json b/homeassistant/components/bmw_connected_drive/translations/pt-BR.json new file mode 100644 index 0000000000000..b4b82c1fe1812 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "region": "Regi\u00e3o do ConnectedDrive", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Somente leitura (somente sensores e notificar, sem execu\u00e7\u00e3o de servi\u00e7os, sem bloqueio)", + "use_location": "Use a localiza\u00e7\u00e3o do Home Assistant para pesquisas de localiza\u00e7\u00e3o de carros (necess\u00e1rio para ve\u00edculos n\u00e3o i3/i8 produzidos antes de 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/sk.json b/homeassistant/components/bmw_connected_drive/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 5e782c70868d7..e5f8b004502fb 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -7,5 +7,6 @@ "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["bond_api"] } diff --git a/homeassistant/components/bond/translations/cs.json b/homeassistant/components/bond/translations/cs.json index 6ee951350caf1..1d8e9c720e894 100644 --- a/homeassistant/components/bond/translations/cs.json +++ b/homeassistant/components/bond/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakofigurovan\u00e9" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/bond/translations/el.json b/homeassistant/components/bond/translations/el.json index e430a82e67ee7..8dad035dc3f1f 100644 --- a/homeassistant/components/bond/translations/el.json +++ b/homeassistant/components/bond/translations/el.json @@ -1,9 +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" + }, + "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", + "old_firmware": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03bb\u03b9\u03cc \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Bond - \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03b9\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "flow_title": "{name} ({host})", "step": { "confirm": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } } } } diff --git a/homeassistant/components/bond/translations/pt-BR.json b/homeassistant/components/bond/translations/pt-BR.json index a58a0045e4605..8300c0db0b372 100644 --- a/homeassistant/components/bond/translations/pt-BR.json +++ b/homeassistant/components/bond/translations/pt-BR.json @@ -1,5 +1,28 @@ { "config": { - "flow_title": "Bond: {bond_id} ({host})" + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "old_firmware": "Firmware antigo n\u00e3o suportado no dispositivo Bond - atualize antes de continuar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "Token de acesso" + }, + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "access_token": "Token de acesso", + "host": "Nome do host" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/sk.json b/homeassistant/components/bond/translations/sk.json new file mode 100644 index 0000000000000..e237bd34b0a68 --- /dev/null +++ b/homeassistant/components/bond/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "confirm": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token" + } + }, + "user": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 98fd5ab2d27c3..f4fd65748f16e 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,5 +7,6 @@ "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], - "after_dependencies": ["zeroconf"] + "after_dependencies": ["zeroconf"], + "loggers": ["boschshcpy"] } diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json new file mode 100644 index 0000000000000..72df4a968182f --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/el.json b/homeassistant/components/bosch_shc/translations/el.json index 08d797fd113db..f9b5f4ad3d0b2 100644 --- a/homeassistant/components/bosch_shc/translations/el.json +++ b/homeassistant/components/bosch_shc/translations/el.json @@ -1,7 +1,15 @@ { "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", + "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": { - "pairing_failed": "\u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf Bosch Smart Home Controller \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 (\u03c4\u03bf LED \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03b5\u03b9) \u03ba\u03b1\u03b8\u03ce\u03c2 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc\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", + "pairing_failed": "\u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf Bosch Smart Home Controller \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 (\u03c4\u03bf LED \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03b5\u03b9) \u03ba\u03b1\u03b8\u03ce\u03c2 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc\u03c2.", + "session_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c5\u03bd\u03b5\u03b4\u03c1\u03af\u03b1\u03c2: \u039c\u03b7 \u039f\u039a \u03b1\u03c0\u03bf\u03c4\u03ad\u03bb\u03b5\u03c3\u03bc\u03b1.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "Bosch SHC: {name}", "step": { @@ -14,9 +22,13 @@ } }, "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 bosch_shc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2" + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 bosch_shc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf \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" }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Bosch Smart Home Controller \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant.", "title": "\u03a0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03b9 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 SHC" } diff --git a/homeassistant/components/bosch_shc/translations/pt-BR.json b/homeassistant/components/bosch_shc/translations/pt-BR.json new file mode 100644 index 0000000000000..f8a74157ab358 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/pt-BR.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "pairing_failed": "Falha no emparelhamento; verifique se o Bosch Smart Home Controller est\u00e1 no modo de emparelhamento (LED piscando) e se sua senha est\u00e1 correta.", + "session_error": "Erro de sess\u00e3o: API retorna resultado n\u00e3o OK.", + "unknown": "Erro inesperado" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Pressione o bot\u00e3o frontal do Bosch Smart Home Controller at\u00e9 que o LED comece a piscar.\n Pronto para continuar a configurar {model} @ {host} com o Home Assistant?" + }, + "credentials": { + "data": { + "password": "Senha do Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o bosch_shc precisa re-autenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure seu Bosch Smart Home Controller para permitir monitoramento e controle com o Home Assistant.", + "title": "Par\u00e2metros de autentica\u00e7\u00e3o SHC" + } + } + }, + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/sk.json b/homeassistant/components/bosch_shc/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 38dbc4f0ebc77..3962e9535207a 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -110,7 +110,7 @@ def __init__( ), ) - def _send_command(self, command: str, repeats: int = 1) -> None: + def _send_command(self, command: Iterable[str], repeats: int = 1) -> None: """Send a command to the TV.""" for _ in range(repeats): for cmd in command: diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 18285ebec0014..4ce465abc3673 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -5,5 +5,6 @@ "requirements": ["bravia-tv==1.0.11"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bravia_tv"] } diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index e73f76235f049..489bf0a7c36d5 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -1,17 +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", "no_ip_control": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_host": "\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", "unsupported_model": "\u03a4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." }, "step": { "authorize": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia. \n\n\u0395\u03ac\u03bd \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 -> \u0394\u03af\u03ba\u03c4\u03c5\u03bf -> \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 -> \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 Sony Bravia. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/braviatv \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.", "title": "\u03a4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia" } diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index 1a0fedff9d026..bd6d47af018d6 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -1,7 +1,26 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unsupported_model": "Seu modelo de TV n\u00e3o \u00e9 suportado." + }, "step": { + "authorize": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia.\n\nSe o c\u00f3digo PIN n\u00e3o for mostrado, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", + "title": "Autorizar a TV Sony Bravia" + }, "user": { + "data": { + "host": "Nome do host" + }, "description": "Configure a integra\u00e7\u00e3o do Sony Bravia TV. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/braviatv \n\n Verifique se a sua TV est\u00e1 ligada.", "title": "Sony Bravia TV" } diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 3f7744ecbb445..c1ccc5ec954af 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -31,7 +31,7 @@ "SP4", "SP4B", }, - Platform.LIGHT: {"LB1"}, + Platform.LIGHT: {"LB1", "LB2"}, } DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values()) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 951be9b26bb7a..46582334e2d52 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -12,8 +12,9 @@ NetworkTimeoutError, ) -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -64,13 +65,15 @@ def available(self): return self.update_manager.available @staticmethod - async def async_update(hass, entry): + async def async_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update the device and related entities. Triggered when the device is renamed on the frontend. """ device_registry = dr.async_get(hass) + assert entry.unique_id device_entry = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) + assert device_entry device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 9880d0b3c2237..9af50a345fb46 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -37,7 +37,7 @@ async def async_setup_entry( device = hass.data[DOMAIN].devices[config_entry.entry_id] lights = [] - if device.api.type == "LB1": + if device.api.type in {"LB1", "LB2"}: lights.append(BroadlinkLight(device)) async_add_entities(lights) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 1a6e94003ca24..db1601edd67f8 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -6,6 +6,7 @@ "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ + {"registered_devices": true}, { "macaddress": "34EA34*" }, @@ -19,5 +20,6 @@ "macaddress": "B4430D*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["broadlink"] } diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index aa295fd0d99a8..6a015748bd0ed 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -78,9 +78,8 @@ async def async_setup_platform( """ mac_addr = config[CONF_MAC] host = config.get(CONF_HOST) - switches = config.get(CONF_SWITCHES) - if switches: + if switches := config.get(CONF_SWITCHES): platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) platform_data.setdefault(mac_addr, []).extend(switches) diff --git a/homeassistant/components/broadlink/translations/el.json b/homeassistant/components/broadlink/translations/el.json index 360b9c91d680a..6db366dae32cd 100644 --- a/homeassistant/components/broadlink/translations/el.json +++ b/homeassistant/components/broadlink/translations/el.json @@ -1,9 +1,17 @@ { "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": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03bc\u03b9\u03b1 \u03c1\u03bf\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_host": "\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", - "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" + "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", + "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", + "invalid_host": "\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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name} ( {model} \u03c3\u03c4\u03bf {host} )", "step": { @@ -29,6 +37,7 @@ }, "user": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf" }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/broadlink/translations/es-419.json b/homeassistant/components/broadlink/translations/es-419.json index 9c3129a2c6c0c..1e96b5fee0d08 100644 --- a/homeassistant/components/broadlink/translations/es-419.json +++ b/homeassistant/components/broadlink/translations/es-419.json @@ -21,6 +21,12 @@ }, "description": "{name} ({model} en {host}) est\u00e1 bloqueado. Esto puede provocar problemas de autenticaci\u00f3n en Home Assistant. \u00bfQuieres desbloquearlo?", "title": "Desbloquear el dispositivo (opcional)" + }, + "user": { + "data": { + "timeout": "Tiempo de espera" + }, + "title": "Conectarse al dispositivo" } } } diff --git a/homeassistant/components/broadlink/translations/pt-BR.json b/homeassistant/components/broadlink/translations/pt-BR.json new file mode 100644 index 0000000000000..3872752bb61ca --- /dev/null +++ b/homeassistant/components/broadlink/translations/pt-BR.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "not_supported": "Dispositivo n\u00e3o suportado", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({model} em {host})", + "step": { + "auth": { + "title": "Autenticar no dispositivo" + }, + "finish": { + "data": { + "name": "Nome" + }, + "title": "Escolha um nome para o dispositivo" + }, + "reset": { + "description": "{name} ({model} em {host}) est\u00e1 bloqueado. Voc\u00ea precisa desbloquear o dispositivo para autenticar e completar a configura\u00e7\u00e3o. Instru\u00e7\u00f5es:\n1. Abra o aplicativo Broadlink.\n2. Clique no dispositivo.\n3. Clique em '...' no canto superior direito.\n4. Role at\u00e9 a parte inferior da p\u00e1gina.\n5. Desabilite o bloqueio.", + "title": "Desbloqueie o dispositivo" + }, + "unlock": { + "data": { + "unlock": "Sim, fa\u00e7a isso." + }, + "description": "{name} ({model} em {host}) est\u00e1 bloqueado. Isso pode levar a problemas de autentica\u00e7\u00e3o no Home Assistant. Gostaria de desbloque\u00e1-lo?", + "title": "Desbloquear o dispositivo (opcional)" + }, + "user": { + "data": { + "host": "Nome do host", + "timeout": "Tempo esgotado" + }, + "title": "Conectar-se ao dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/sk.json b/homeassistant/components/broadlink/translations/sk.json new file mode 100644 index 0000000000000..358fdc848ff09 --- /dev/null +++ b/homeassistant/components/broadlink/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "finish": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 29020b1e90562..2b98b757fbd37 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -17,6 +17,7 @@ def get_update_manager(device): "A1": BroadlinkA1UpdateManager, "BG1": BroadlinkBG1UpdateManager, "LB1": BroadlinkLB1UpdateManager, + "LB2": BroadlinkLB1UpdateManager, "MP1": BroadlinkMP1UpdateManager, "RM4MINI": BroadlinkRMUpdateManager, "RM4PRO": BroadlinkRMUpdateManager, diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 77a84c70de84d..aaf1af72db9ef 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -12,5 +12,6 @@ ], "config_flow": true, "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"] } diff --git a/homeassistant/components/brother/translations/el.json b/homeassistant/components/brother/translations/el.json index 935a234be8527..f8a27353a6af5 100644 --- a/homeassistant/components/brother/translations/el.json +++ b/homeassistant/components/brother/translations/el.json @@ -1,6 +1,7 @@ { "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", "unsupported_model": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." }, "error": { @@ -12,6 +13,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", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae Brother. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/brother" diff --git a/homeassistant/components/brother/translations/pt-BR.json b/homeassistant/components/brother/translations/pt-BR.json index e7ee63e6e5bf1..33113d12881a3 100644 --- a/homeassistant/components/brother/translations/pt-BR.json +++ b/homeassistant/components/brother/translations/pt-BR.json @@ -1,19 +1,29 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "unsupported_model": "Este modelo de impressora n\u00e3o \u00e9 suportado." }, "error": { + "cannot_connect": "Falha ao conectar", "snmp_error": "Servidor SNMP desligado ou impressora n\u00e3o suportada.", "wrong_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido." }, + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { - "host": "Nome do host ou endere\u00e7o IP da impressora", + "host": "Nome do host", "type": "Tipo de impressora" }, "description": "Configure a integra\u00e7\u00e3o da impressora Brother. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo de impressora" + }, + "description": "Deseja adicionar a impressora Brother {model} com n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", + "title": "Impressora Brother descoberta" } } } diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index cb91446e4762f..693d6ab465ceb 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": ["brottsplatskartan==0.0.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["brottsplatskartan"] } diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 5fe3f7d0012ae..988a96ce08e8b 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_BAPI, DATA_COOR, DOMAIN, PLATFORMS, REGULAR_INTERVAL @@ -20,9 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Brunt using config flow.""" + session = async_get_clientsession(hass) bapi = BruntClientAsync( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], + session=session, ) try: await bapi.async_login() diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index fce775d4b7d89..11bafbca07b50 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,7 +3,8 @@ "name": "Brunt Blind Engine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": ["brunt==1.1.1"], + "requirements": ["brunt==1.2.0"], "codeowners": ["@eavanvalkenburg"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["brunt"] } diff --git a/homeassistant/components/brunt/translations/cs.json b/homeassistant/components/brunt/translations/cs.json new file mode 100644 index 0000000000000..72df4a968182f --- /dev/null +++ b/homeassistant/components/brunt/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/el.json b/homeassistant/components/brunt/translations/el.json index 95becd2dc0aa9..a7a676b4ed9c8 100644 --- a/homeassistant/components/brunt/translations/el.json +++ b/homeassistant/components/brunt/translations/el.json @@ -1,10 +1,27 @@ { "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", + "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", + "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": { "reauth_confirm": { - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd: {username}" + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd: {username}", + "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": { + "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" + }, "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Brunt" } } diff --git a/homeassistant/components/brunt/translations/es-419.json b/homeassistant/components/brunt/translations/es-419.json new file mode 100644 index 0000000000000..a461c335f5545 --- /dev/null +++ b/homeassistant/components/brunt/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Vuelva a ingresar la contrase\u00f1a para: {username}" + }, + "user": { + "title": "Configura tu integraci\u00f3n Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/nb.json b/homeassistant/components/brunt/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/brunt/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/pt-BR.json b/homeassistant/components/brunt/translations/pt-BR.json new file mode 100644 index 0000000000000..6368184212aaf --- /dev/null +++ b/homeassistant/components/brunt/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Por favor, reinsira a senha para: {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Configure sua integra\u00e7\u00e3o Brunt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/sk.json b/homeassistant/components/brunt/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/brunt/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index b1762f7fea319..88eefb7f9c023 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/bsblan", "requirements": ["bsblan==0.5.0"], "codeowners": ["@liudger"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bsblan"] } diff --git a/homeassistant/components/bsblan/translations/el.json b/homeassistant/components/bsblan/translations/el.json index 04b238a916d22..b3d9cbb2bfab6 100644 --- a/homeassistant/components/bsblan/translations/el.json +++ b/homeassistant/components/bsblan/translations/el.json @@ -1,7 +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", + "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": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "passkey": "\u03a3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "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" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae BSB-Lan \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant.", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae BSB-Lan" + } } } } \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index dda5e5c293c14..685cbe686bb76 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/bsblan/translations/id.json b/homeassistant/components/bsblan/translations/id.json index 83fdb88aae4ff..8d30e37749f6f 100644 --- a/homeassistant/components/bsblan/translations/id.json +++ b/homeassistant/components/bsblan/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/bsblan/translations/nl.json b/homeassistant/components/bsblan/translations/nl.json index e07f6bc25f049..742aaa9f842a9 100644 --- a/homeassistant/components/bsblan/translations/nl.json +++ b/homeassistant/components/bsblan/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json index 043477ed3f92e..64012b146377d 100644 --- a/homeassistant/components/bsblan/translations/no.json +++ b/homeassistant/components/bsblan/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" }, "error": { "cannot_connect": "Tilkobling mislyktes" @@ -16,7 +17,7 @@ "port": "Port", "username": "Brukernavn" }, - "description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.", + "description": "Konfigurer BSB-Lan-enheten din for \u00e5 integreres med Home Assistant.", "title": "Koble til BSB-Lan-enheten" } } diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json index 3667c2432bfca..c442cda74684b 100644 --- a/homeassistant/components/bsblan/translations/pl.json +++ b/homeassistant/components/bsblan/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "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" diff --git a/homeassistant/components/bsblan/translations/pt-BR.json b/homeassistant/components/bsblan/translations/pt-BR.json index 3f8701092d187..789b09ebefce2 100644 --- a/homeassistant/components/bsblan/translations/pt-BR.json +++ b/homeassistant/components/bsblan/translations/pt-BR.json @@ -1,7 +1,22 @@ { "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": { + "host": "Nome do host", + "passkey": "Chave da senha", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, "description": "Configure o seu dispositivo BSB-Lan para integrar com o Home Assistant.", "title": "Conecte-se ao dispositivo BSB-Lan" } diff --git a/homeassistant/components/bsblan/translations/sk.json b/homeassistant/components/bsblan/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/bsblan/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json new file mode 100644 index 0000000000000..46631acc69a79 --- /dev/null +++ b/homeassistant/components/bsblan/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index dfd61b1b9a849..e0edcd934e6bd 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": ["bthomehub5-devicelist==0.1.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bthomehub5_devicelist"] } diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 33fab430453a2..6a0453752e9f6 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": ["btsmarthub_devicelist==0.2.0"], "codeowners": ["@jxwolstenholme"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["btsmarthub_devicelist"] } diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index f88bfb83ddf26..68011bb7bb223 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": ["buienradar==1.0.5"], "codeowners": ["@mjj4791", "@ties", "@Robbie1221"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["buienradar", "vincenty"] } diff --git a/homeassistant/components/buienradar/translations/cs.json b/homeassistant/components/buienradar/translations/cs.json new file mode 100644 index 0000000000000..31db40bd1607e --- /dev/null +++ b/homeassistant/components/buienradar/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "error": { + "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "data": { + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "K\u00f3d zem\u011b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/el.json b/homeassistant/components/buienradar/translations/el.json index b96d49b8615e5..387ba0d746039 100644 --- a/homeassistant/components/buienradar/translations/el.json +++ b/homeassistant/components/buienradar/translations/el.json @@ -1,4 +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" + }, + "error": { + "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" + }, + "step": { + "user": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/buienradar/translations/es-419.json b/homeassistant/components/buienradar/translations/es-419.json new file mode 100644 index 0000000000000..95968bce45c60 --- /dev/null +++ b/homeassistant/components/buienradar/translations/es-419.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "country_code": "C\u00f3digo de pa\u00eds del pa\u00eds para mostrar las im\u00e1genes de la c\u00e1mara.", + "delta": "Intervalo de tiempo en segundos entre las actualizaciones de la imagen de la c\u00e1mara", + "timeframe": "Minutos para anticipar el pron\u00f3stico de precipitaci\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/pt-BR.json b/homeassistant/components/buienradar/translations/pt-BR.json new file mode 100644 index 0000000000000..9ce1013644ffc --- /dev/null +++ b/homeassistant/components/buienradar/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "C\u00f3digo do pa\u00eds para exibir as imagens da c\u00e2mera.", + "delta": "Intervalo de tempo em segundos entre as atualiza\u00e7\u00f5es da imagem da c\u00e2mera", + "timeframe": "Minutos para antecipar a previs\u00e3o de precipita\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/sk.json b/homeassistant/components/buienradar/translations/sk.json new file mode 100644 index 0000000000000..d77712e768a3a --- /dev/null +++ b/homeassistant/components/buienradar/translations/sk.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "K\u00f3d krajiny" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/el.json b/homeassistant/components/button/translations/el.json new file mode 100644 index 0000000000000..bfbd1c2d210b4 --- /dev/null +++ b/homeassistant/components/button/translations/el.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b1\u03c4\u03b7\u03b8\u03b5\u03af" + } + }, + "title": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af" +} \ No newline at end of file diff --git a/homeassistant/components/button/translations/es-419.json b/homeassistant/components/button/translations/es-419.json new file mode 100644 index 0000000000000..e3a46094da9ee --- /dev/null +++ b/homeassistant/components/button/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "press": "Presiona el bot\u00f3n {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 06e90d31942b5..91f563107ed49 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": ["caldav==0.8.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["caldav", "vobject"] } diff --git a/homeassistant/components/calendar/translations/pt-BR.json b/homeassistant/components/calendar/translations/pt-BR.json index fca0b1a103bd9..0d47e41440b50 100644 --- a/homeassistant/components/calendar/translations/pt-BR.json +++ b/homeassistant/components/calendar/translations/pt-BR.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inativo", - "on": "Ativo" + "off": "Desligado", + "on": "Ligado" } }, "title": "Calend\u00e1rio" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 466dce1c84d2b..b955f1a0249bf 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -4,7 +4,7 @@ import asyncio import base64 import collections -from collections.abc import Awaitable, Callable, Iterable, Mapping +from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress from dataclasses import dataclass from datetime import datetime, timedelta @@ -26,7 +26,6 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_EXTRA, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) @@ -49,7 +48,7 @@ PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity, EntityDescription, entity_sources +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType @@ -979,56 +978,22 @@ async def async_handle_play_stream_service( camera: Camera, service_call: ServiceCall ) -> None: """Handle play stream services calls.""" + hass = camera.hass fmt = service_call.data[ATTR_FORMAT] url = await _async_stream_endpoint_url(camera.hass, camera, fmt) - - hass = camera.hass - data: Mapping[str, str] = { - ATTR_MEDIA_CONTENT_ID: f"{get_url(hass)}{url}", - ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], - } - - # It is required to send a different payload for cast media players - entity_ids = service_call.data[ATTR_MEDIA_PLAYER] - sources = entity_sources(hass) - cast_entity_ids = [ - entity - for entity in entity_ids - # All entities should be in sources. This extra guard is to - # avoid people writing to the state machine and breaking it. - if entity in sources and sources[entity]["domain"] == "cast" - ] - other_entity_ids = list(set(entity_ids) - set(cast_entity_ids)) - - if cast_entity_ids: - await hass.services.async_call( - DOMAIN_MP, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: cast_entity_ids, - **data, - ATTR_MEDIA_EXTRA: { - "stream_type": "LIVE", - "media_info": { - "hlsVideoSegmentFormat": "fmp4", - }, - }, - }, - blocking=True, - context=service_call.context, - ) - - if other_entity_ids: - await hass.services.async_call( - DOMAIN_MP, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: other_entity_ids, - **data, - }, - blocking=True, - context=service_call.context, - ) + url = f"{get_url(hass)}{url}" + + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: service_call.data[ATTR_MEDIA_PLAYER], + ATTR_MEDIA_CONTENT_ID: url, + ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], + }, + blocking=True, + context=service_call.context, + ) async def _async_stream_endpoint_url( diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py new file mode 100644 index 0000000000000..e65aabe459de6 --- /dev/null +++ b/homeassistant/components/camera/media_source.py @@ -0,0 +1,120 @@ +"""Expose cameras as media sources.""" +from __future__ import annotations + +from typing import Optional, cast + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_component import EntityComponent + +from . import Camera, _async_stream_endpoint_url +from .const import DOMAIN, STREAM_TYPE_HLS + + +async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource: + """Set up camera media source.""" + return CameraMediaSource(hass) + + +class CameraMediaSource(MediaSource): + """Provide camera feeds as media sources.""" + + name: str = "Camera" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize CameraMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + component: EntityComponent = self.hass.data[DOMAIN] + camera = cast(Optional[Camera], component.get_entity(item.identifier)) + + if not camera: + raise Unresolvable(f"Could not resolve media item: {item.identifier}") + + if (stream_type := camera.frontend_stream_type) is None: + return PlayMedia( + f"/api/camera_proxy_stream/{camera.entity_id}", camera.content_type + ) + + if stream_type != STREAM_TYPE_HLS: + raise Unresolvable("Camera does not support MJPEG or HLS streaming.") + + if "stream" not in self.hass.config.components: + raise Unresolvable("Stream integration not loaded") + + try: + url = await _async_stream_endpoint_url(self.hass, camera, HLS_PROVIDER) + except HomeAssistantError as err: + raise Unresolvable(str(err)) from err + + return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER]) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if item.identifier: + raise BrowseError("Unknown item") + + can_stream_hls = "stream" in self.hass.config.components + + # Root. List cameras. + component: EntityComponent = self.hass.data[DOMAIN] + children = [] + not_shown = 0 + for camera in component.entities: + camera = cast(Camera, camera) + stream_type = camera.frontend_stream_type + + if stream_type is None: + content_type = camera.content_type + + elif can_stream_hls and stream_type == STREAM_TYPE_HLS: + content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + + else: + not_shown += 1 + continue + + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=camera.entity_id, + media_class=MEDIA_CLASS_VIDEO, + media_content_type=content_type, + title=camera.name, + thumbnail=f"/api/camera_proxy/{camera.entity_id}", + can_play=True, + can_expand=False, + ) + ) + + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_APP, + media_content_type="", + title="Camera", + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + children=children, + not_shown=not_shown, + ) diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index 967273a0f3426..6b3176f6bbd4b 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -12,7 +12,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -51,7 +50,7 @@ def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: return CanaryOptionsFlowHandler(config_entry) async def async_step_import( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index c9a75b063f611..12b4d54b39156 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["canary"] } diff --git a/homeassistant/components/canary/translations/es-419.json b/homeassistant/components/canary/translations/es-419.json index 8ce6a8fb855a2..57cc475b383de 100644 --- a/homeassistant/components/canary/translations/es-419.json +++ b/homeassistant/components/canary/translations/es-419.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "title": "Conectarse a Canary" diff --git a/homeassistant/components/canary/translations/pt-BR.json b/homeassistant/components/canary/translations/pt-BR.json new file mode 100644 index 0000000000000..3f3d51547574f --- /dev/null +++ b/homeassistant/components/canary/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se ao Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumentos passados para ffmpeg para c\u00e2meras", + "timeout": "Tempo limite da solicita\u00e7\u00e3o (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/uk.json b/homeassistant/components/canary/translations/uk.json index 74327f3ebd672..6664c756e16b8 100644 --- a/homeassistant/components/canary/translations/uk.json +++ b/homeassistant/components/canary/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "error": { diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json index 6c7dbea4daad7..689ff1c42c505 100644 --- a/homeassistant/components/canary/translations/zh-Hant.json +++ b/homeassistant/components/canary/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 86e5557160cb4..d0b7cfc415850 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) @@ -113,3 +113,13 @@ async def _register_cast_platform( async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Remove Home Assistant Cast user.""" await home_assistant_cast.async_remove_user(hass, entry) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove cast config entry from a device. + + The actual cleanup is done in CastMediaPlayerEntity.async_will_remove_from_hass. + """ + return True diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index ba7380bcaa208..ad36fb4e33976 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -81,8 +81,8 @@ def get_zeroconf(cls): class CastStatusListener: """Helper class to handle pychromecast status callbacks. - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can + Necessary because a CastDevice entity or dynamic group can create a new + socket client and therefore callbacks from multiple chromecast connections can potentially arrive. This class allows invalidating past chromecast objects. """ diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 0f312d6a37a7c..2f9583f329c05 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -6,14 +6,16 @@ from homeassistant import auth, config_entries, core from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, dispatcher -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW SERVICE_SHOW_VIEW = "show_lovelace_view" ATTR_VIEW_PATH = "view_path" ATTR_URL_PATH = "dashboard_path" +NO_URL_AVAILABLE_ERROR = "Home Assistant Cast requires your instance to be reachable via HTTPS. Enable Home Assistant Cloud or set up an external URL with valid SSL certificates" async def async_setup_ha_cast( @@ -41,7 +43,10 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall) -> None: """Handle a Show View service call.""" - hass_url = get_url(hass, require_ssl=True, prefer_external=True) + try: + hass_url = get_url(hass, require_ssl=True, prefer_external=True) + except NoURLAvailableError as err: + raise HomeAssistantError(NO_URL_AVAILABLE_ERROR) from err controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b1a4cd0b35874..2316a884b7349 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,5 +14,6 @@ ], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["casttube", "pychromecast"] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index d418373e599ad..091bde53ae0bb 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -2,11 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from contextlib import suppress -from datetime import datetime, timedelta +from datetime import datetime import json import logging -from urllib.parse import quote import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -18,13 +18,14 @@ CONNECTION_STATUS_DISCONNECTED, ) import voluptuous as vol +import yarl from homeassistant.components import media_source, zeroconf -from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( BrowseError, BrowseMedia, MediaPlayerEntity, + async_process_play_media_url, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, @@ -59,7 +60,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.network import NoURLAvailableError, get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url, is_hass_url import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -96,7 +97,7 @@ @callback def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): - """Create a CastDevice Entity from the chromecast object. + """Create a CastDevice entity or dynamic group from the chromecast object. Returns None if the cast device has already been added. """ @@ -120,7 +121,7 @@ def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): group.async_setup() return None - return CastDevice(info) + return CastMediaPlayerEntity(hass, info) async def async_setup_entry( @@ -154,81 +155,63 @@ def async_cast_discovered(discover: ChromecastInfo) -> None: hass.async_add_executor_job(setup_internal_discovery, hass, config_entry) -class CastDevice(MediaPlayerEntity): - """Representation of a Cast device on the network. +class CastDevice: + """Representation of a Cast device or dynamic group on the network. This class is the holder of the pychromecast.Chromecast object and its - socket client. It therefore handles all reconnects and audio group changing + socket client. It therefore handles all reconnects and audio groups changing "elected leader" itself. """ - _attr_should_poll = False - _attr_media_image_remotely_accessible = True + _mz_only: bool - def __init__(self, cast_info: ChromecastInfo) -> None: + def __init__(self, hass: HomeAssistant, cast_info: ChromecastInfo) -> None: """Initialize the cast device.""" + self.hass: HomeAssistant = hass self._cast_info = cast_info self._chromecast: pychromecast.Chromecast | None = None - self.cast_status = None - self.media_status = None - self.media_status_received = None - self.mz_media_status: dict[str, pychromecast.controllers.media.MediaStatus] = {} - self.mz_media_status_received: dict[str, datetime] = {} self.mz_mgr = None - self._attr_available = False self._status_listener: CastStatusListener | None = None - self._hass_cast_controller: HomeAssistantController | None = None - - self._add_remove_handler = None - self._cast_view_remove_handler = None - self._attr_unique_id = str(cast_info.uuid) - self._attr_name = cast_info.friendly_name - if cast_info.cast_info.model_name != "Google Cast Group": - self._attr_device_info = DeviceInfo( - identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, - manufacturer=str(cast_info.cast_info.manufacturer), - model=cast_info.cast_info.model_name, - name=str(cast_info.friendly_name), - ) + self._add_remove_handler: Callable[[], None] | None = None + self._del_remove_handler: Callable[[], None] | None = None + self._name: str | None = None - async def async_added_to_hass(self): - """Create chromecast object when added to hass.""" + def _async_setup(self, name: str) -> None: + """Create chromecast object.""" + self._name = name self._add_remove_handler = async_dispatcher_connect( self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) + self._del_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed + ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) - self.async_set_cast_info(self._cast_info) # asyncio.create_task is used to avoid delaying startup wrapup if the device # is discovered already during startup but then fails to respond asyncio.create_task( - async_create_catching_coro(self.async_connect_to_chromecast()) - ) - - self._cast_view_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view + async_create_catching_coro(self._async_connect_to_chromecast()) ) - async def async_will_remove_from_hass(self) -> None: - """Disconnect Chromecast object when removed.""" + async def _async_tear_down(self) -> None: + """Disconnect chromecast object and remove listeners.""" await self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) if self._add_remove_handler: self._add_remove_handler() self._add_remove_handler = None - if self._cast_view_remove_handler: - self._cast_view_remove_handler() - self._cast_view_remove_handler = None - - def async_set_cast_info(self, cast_info): - """Set the cast information.""" - self._cast_info = cast_info + if self._del_remove_handler: + self._del_remove_handler() + self._del_remove_handler = None - async def async_connect_to_chromecast(self): + async def _async_connect_to_chromecast(self): """Set up the chromecast object.""" - _LOGGER.debug( "[%s %s] Connecting to cast device by service %s", - self.entity_id, + self._name, self._cast_info.friendly_name, self._cast_info.cast_info.services, ) @@ -244,45 +227,120 @@ async def async_connect_to_chromecast(self): self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] - self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr) - self._attr_available = False - self.cast_status = chromecast.status - self.media_status = chromecast.media_controller.status + self._status_listener = CastStatusListener( + self, chromecast, self.mz_mgr, self._mz_only + ) self._chromecast.start() - self.async_write_ha_state() async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" - if self._chromecast is None: - # Can't disconnect if not connected. + if self._chromecast is not None: + _LOGGER.debug( + "[%s %s] Disconnecting from chromecast socket", + self._name, + self._cast_info.friendly_name, + ) + await self.hass.async_add_executor_job(self._chromecast.disconnect) + + self._invalidate() + + def _invalidate(self): + """Invalidate some attributes.""" + self._chromecast = None + self.mz_mgr = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + async def _async_cast_discovered(self, discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. return - _LOGGER.debug( - "[%s %s] Disconnecting from chromecast socket", - self.entity_id, - self._cast_info.friendly_name, + + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + self._cast_info = discover + + async def _async_cast_removed(self, discover: ChromecastInfo): + """Handle removal of Chromecast.""" + + async def _async_stop(self, event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + + +class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): + """Representation of a Cast device on the network.""" + + _attr_should_poll = False + _attr_media_image_remotely_accessible = True + _mz_only = False + + def __init__(self, hass: HomeAssistant, cast_info: ChromecastInfo) -> None: + """Initialize the cast device.""" + + CastDevice.__init__(self, hass, cast_info) + + self.cast_status = None + self.media_status = None + self.media_status_received = None + self.mz_media_status: dict[str, pychromecast.controllers.media.MediaStatus] = {} + self.mz_media_status_received: dict[str, datetime] = {} + self._attr_available = False + self._hass_cast_controller: HomeAssistantController | None = None + + self._cast_view_remove_handler = None + self._attr_unique_id = str(cast_info.uuid) + self._attr_name = cast_info.friendly_name + self._attr_device_info = DeviceInfo( + identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, + manufacturer=str(cast_info.cast_info.manufacturer), + model=cast_info.cast_info.model_name, + name=str(cast_info.friendly_name), ) + + async def async_added_to_hass(self): + """Create chromecast object when added to hass.""" + self._async_setup(self.entity_id) + + self._cast_view_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect Chromecast object when removed.""" + await self._async_tear_down() + + if self._cast_view_remove_handler: + self._cast_view_remove_handler() + self._cast_view_remove_handler = None + + async def _async_connect_to_chromecast(self): + """Set up the chromecast object.""" + await super()._async_connect_to_chromecast() + self._attr_available = False + self.cast_status = self._chromecast.status + self.media_status = self._chromecast.media_controller.status self.async_write_ha_state() - await self.hass.async_add_executor_job(self._chromecast.disconnect) - - self._invalidate() + async def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + await super()._async_disconnect() + self._attr_available = False self.async_write_ha_state() def _invalidate(self): """Invalidate some attributes.""" - self._chromecast = None + super()._invalidate() + self.cast_status = None self.media_status = None self.media_status_received = None self.mz_media_status = {} self.mz_media_status_received = {} - self.mz_mgr = None self._hass_cast_controller = None - if self._status_listener is not None: - self._status_listener.invalidate() - self._status_listener = None # ========== Callbacks ========== def new_cast_status(self, cast_status): @@ -411,7 +469,7 @@ def turn_on(self): # The only way we can turn the Chromecast is on is by launching an app if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: - self._chromecast.play_media(CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + self._chromecast.play_media(CAST_SPLASH, "image/png") else: self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) @@ -473,13 +531,13 @@ async def _async_root_payload(self, content_filter): result = await media_source.async_browse_media( self.hass, None, content_filter=content_filter ) - children.append(result) + children.extend(result.children) except BrowseError: if not children: raise # If there's only one media source, resolve it - if len(children) == 1: + if len(children) == 1 and children[0].can_expand: return await self.async_browse_media( children[0].media_content_type, children[0].media_content_id, @@ -492,7 +550,7 @@ async def _async_root_payload(self, content_filter): media_content_type="", can_play=False, can_expand=True, - children=children, + children=sorted(children, key=lambda c: c.title), ) async def async_browse_media(self, media_content_type=None, media_content_id=None): @@ -535,19 +593,6 @@ async def async_play_media(self, media_type, media_id, **kwargs): media_type = sourced_media.mime_type media_id = sourced_media.url - # If media ID is a relative URL, we serve it from HA. - # Create a signed path. - if media_id[0] == "/": - media_id = async_sign_path( - self.hass, - quote(media_id), - timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), - ) - - # prepend external URL - hass_url = get_url(self.hass, prefer_external=True) - media_id = f"{hass_url}{media_id}" - extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) metadata = extra.get("metadata") @@ -593,6 +638,21 @@ async def async_play_media(self, media_type, media_id, **kwargs): if result: return + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) + + # Configure play command for when playing a HLS stream + if is_hass_url(self.hass, media_id): + parsed = yarl.URL(media_id) + if parsed.path.startswith("/api/hls/"): + extra = { + **extra, + "stream_type": "LIVE", + "media_info": { + "hlsVideoSegmentFormat": "fmp4", + }, + } + # Default to play with the default media receiver app_data = {"media_id": media_id, "media_type": media_type, **extra} await self.hass.async_add_executor_job( @@ -797,19 +857,6 @@ def media_position_updated_at(self): return None return self._media_status()[1] - async def _async_cast_discovered(self, discover: ChromecastInfo): - """Handle discovery of new Chromecast.""" - if self._cast_info.uuid != discover.uuid: - # Discovered is not our device. - return - - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.async_set_cast_info(discover) - - async def _async_stop(self, event): - """Disconnect socket on Home Assistant stop.""" - await self._async_disconnect() - def _handle_signal_show_view( self, controller: HomeAssistantController, @@ -828,109 +875,14 @@ def _handle_signal_show_view( self._hass_cast_controller.show_lovelace_view(view_path, url_path) -class DynamicCastGroup: +class DynamicCastGroup(CastDevice): """Representation of a Cast device on the network - for dynamic cast groups.""" - def __init__(self, hass, cast_info: ChromecastInfo): - """Initialize the cast device.""" - - self.hass = hass - self._cast_info = cast_info - self._chromecast: pychromecast.Chromecast | None = None - self.mz_mgr = None - self._status_listener: CastStatusListener | None = None - - self._add_remove_handler = None - self._del_remove_handler = None + _mz_only = True def async_setup(self): """Create chromecast object.""" - self._add_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered - ) - self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed - ) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) - self.async_set_cast_info(self._cast_info) - self.hass.async_create_task( - async_create_catching_coro(self.async_connect_to_chromecast()) - ) - - async def async_tear_down(self) -> None: - """Disconnect Chromecast object.""" - await self._async_disconnect() - if self._cast_info.uuid is not None: - # Remove the entity from the added casts so that it can dynamically - # be re-added again. - self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) - if self._add_remove_handler: - self._add_remove_handler() - self._add_remove_handler = None - if self._del_remove_handler: - self._del_remove_handler() - self._del_remove_handler = None - - def async_set_cast_info(self, cast_info): - """Set the cast information and set up the chromecast object.""" - - self._cast_info = cast_info - - async def async_connect_to_chromecast(self): - """Set the cast information and set up the chromecast object.""" - - _LOGGER.debug( - "[%s %s] Connecting to cast device by service %s", - "Dynamic group", - self._cast_info.friendly_name, - self._cast_info.cast_info.services, - ) - chromecast = await self.hass.async_add_executor_job( - pychromecast.get_chromecast_from_cast_info, - self._cast_info.cast_info, - ChromeCastZeroconf.get_zeroconf(), - ) - self._chromecast = chromecast - - if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: - self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() - - self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] - - self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr, True) - self._chromecast.start() - - async def _async_disconnect(self): - """Disconnect Chromecast object if it is set.""" - if self._chromecast is None: - # Can't disconnect if not connected. - return - _LOGGER.debug( - "[%s %s] Disconnecting from chromecast socket", - "Dynamic group", - self._cast_info.friendly_name, - ) - - await self.hass.async_add_executor_job(self._chromecast.disconnect) - - self._invalidate() - - def _invalidate(self): - """Invalidate some attributes.""" - self._chromecast = None - self.mz_mgr = None - if self._status_listener is not None: - self._status_listener.invalidate() - self._status_listener = None - - async def _async_cast_discovered(self, discover: ChromecastInfo): - """Handle discovery of new Chromecast.""" - if self._cast_info.uuid != discover.uuid: - # Discovered is not our device. - return - - _LOGGER.debug("Discovered dynamic group with same UUID: %s", discover) - self.async_set_cast_info(discover) + self._async_setup("Dynamic group") async def _async_cast_removed(self, discover: ChromecastInfo): """Handle removal of Chromecast.""" @@ -941,8 +893,4 @@ async def _async_cast_removed(self, discover: ChromecastInfo): if not discover.cast_info.services: # Clean up the dynamic group _LOGGER.debug("Clean up dynamic group: %s", discover) - await self.async_tear_down() - - async def _async_stop(self, event): - """Disconnect socket on Home Assistant stop.""" - await self._async_disconnect() + await self._async_tear_down() diff --git a/homeassistant/components/cast/translations/el.json b/homeassistant/components/cast/translations/el.json index 7a6ecd18b9fd9..b3e45927b9b63 100644 --- a/homeassistant/components/cast/translations/el.json +++ b/homeassistant/components/cast/translations/el.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "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": { "invalid_known_hosts": "\u039f\u03b9 \u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03af \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03af \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ce\u03bd \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1." }, @@ -10,6 +13,9 @@ }, "description": "\u0393\u03bd\u03c9\u03c3\u03c4\u03bf\u03af \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03af \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2 - \u039c\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1 \u03c4\u03c9\u03bd \u03bf\u03bd\u03bf\u03bc\u03ac\u03c4\u03c9\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ce\u03bd \u03ae \u03c4\u03c9\u03bd \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd IP \u03c4\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd cast, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 mDNS \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af.", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Google Cast" + }, + "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;" } } }, diff --git a/homeassistant/components/cast/translations/pt-BR.json b/homeassistant/components/cast/translations/pt-BR.json index 8abd2dac5e5f4..708a507225433 100644 --- a/homeassistant/components/cast/translations/pt-BR.json +++ b/homeassistant/components/cast/translations/pt-BR.json @@ -1,11 +1,43 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_known_hosts": "Os hosts conhecidos devem ser uma lista de hosts separados por v\u00edrgulas." }, "step": { + "config": { + "data": { + "known_hosts": "Hosts conhecidos" + }, + "description": "Hosts conhecidos - Uma lista separada por v\u00edrgulas de nomes de host ou endere\u00e7os IP de dispositivos de transmiss\u00e3o, use se a descoberta de mDNS n\u00e3o estiver funcionando.", + "title": "Configura\u00e7\u00e3o do Google Cast" + }, "confirm": { - "description": "Deseja configurar o Google Cast?" + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "options": { + "error": { + "invalid_known_hosts": "Os hosts conhecidos devem ser uma lista de hosts separados por v\u00edrgulas." + }, + "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorar CEC", + "uuid": "UUIDs permitidos" + }, + "description": "UUIDs permitidos - uma lista separada por v\u00edrgulas de UUIDs de dispositivos Cast para adicionar ao Home Assistant. Use somente se n\u00e3o quiser adicionar todos os dispositivos de transmiss\u00e3o dispon\u00edveis.\n Ignore CEC - Uma lista separada por v\u00edrgulas de Chromecasts que devem ignorar os dados CEC para determinar a entrada ativa. Isso ser\u00e1 passado para pychromecast.IGNORE_CEC.", + "title": "Configura\u00e7\u00e3o avan\u00e7ada do Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Anfitri\u00f5es conhecidos" + }, + "description": "Hosts conhecidos - Uma lista separada por v\u00edrgulas de nomes de host ou endere\u00e7os IP de dispositivos de transmiss\u00e3o, use se a descoberta de mDNS n\u00e3o estiver funcionando.", + "title": "Configura\u00e7\u00e3o do Google Cast" } } } diff --git a/homeassistant/components/cast/translations/sk.json b/homeassistant/components/cast/translations/sk.json new file mode 100644 index 0000000000000..e227301685bbd --- /dev/null +++ b/homeassistant/components/cast/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/uk.json b/homeassistant/components/cast/translations/uk.json index 5f8d69f5f29b8..5ee7dbfde346d 100644 --- a/homeassistant/components/cast/translations/uk.json +++ b/homeassistant/components/cast/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 1994465c410fd..cc538845603fe 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002" diff --git a/homeassistant/components/cert_expiry/translations/el.json b/homeassistant/components/cert_expiry/translations/el.json index 12b612a8c1038..c731fb0834ada 100644 --- a/homeassistant/components/cert_expiry/translations/el.json +++ b/homeassistant/components/cert_expiry/translations/el.json @@ -1,10 +1,24 @@ { "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", "import_failed": "\u0397 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5" }, "error": { - "connection_refused": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae" + "connection_refused": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae", + "connection_timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae", + "resolve_failed": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03c5\u03b8\u03b5\u03af" + }, + "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": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03bf\u03c2 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ae" + } } - } + }, + "title": "\u039b\u03ae\u03be\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd" } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/pt-BR.json b/homeassistant/components/cert_expiry/translations/pt-BR.json index 6a395059625ce..6e31e42ed4957 100644 --- a/homeassistant/components/cert_expiry/translations/pt-BR.json +++ b/homeassistant/components/cert_expiry/translations/pt-BR.json @@ -1,15 +1,20 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "import_failed": "Falha na importa\u00e7\u00e3o da configura\u00e7\u00e3o" + }, "error": { + "connection_refused": "Conex\u00e3o recusada ao se conectar ao host", "connection_timeout": "Tempo limite ao conectar-se a este host", "resolve_failed": "Este host n\u00e3o pode ser resolvido" }, "step": { "user": { "data": { - "host": "O nome do host do certificado", + "host": "Nome do host", "name": "O nome do certificado", - "port": "A porta do certificado" + "port": "Porta" }, "title": "Defina o certificado para testar" } diff --git a/homeassistant/components/cert_expiry/translations/sk.json b/homeassistant/components/cert_expiry/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 1113699cdcac9..d167d6b420788 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": ["pychannels==1.0.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pychannels"] } diff --git a/homeassistant/components/circuit/manifest.json b/homeassistant/components/circuit/manifest.json index 6c10e7ff29980..da820ccb91fdc 100644 --- a/homeassistant/components/circuit/manifest.json +++ b/homeassistant/components/circuit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/circuit", "codeowners": ["@braam"], "requirements": ["circuit-webhook==1.0.1"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["circuit_webhook"] } diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index 25e07086efe92..651d5eda1af99 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": ["pexpect==4.6.0"], "codeowners": ["@fbradyirl"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pexpect", "ptyprocess"] } diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index e1bdaeb314490..5948bb1f94e98 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": ["ciscomobilityexpress==0.3.9"], "codeowners": ["@fbradyirl"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ciscomobilityexpress"] } diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index ba20014fdcffd..571e7708bc6b3 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": ["webexteamssdk==1.1.1"], "codeowners": ["@fbradyirl"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["webexteamssdk"] } diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 4f0b72a2be84e..d003c693dd0be 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": ["python-clementine-remote==1.0.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["clementineremote"] } diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 69567bf65fc5b..7ee804f42f188 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -260,7 +260,7 @@ def __post_init__(self) -> None: name="Particulate Matter < 2.5 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399 ** 3, + metric_conversion=3.2808399**3, is_metric_check=True, ), ClimaCellSensorEntityDescription( @@ -268,7 +268,7 @@ def __post_init__(self) -> None: name="Particulate Matter < 10 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399 ** 3, + metric_conversion=3.2808399**3, is_metric_check=True, ), ClimaCellSensorEntityDescription( @@ -424,7 +424,7 @@ def __post_init__(self) -> None: name="Particulate Matter < 2.5 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399 ** 3, + metric_conversion=3.2808399**3, is_metric_check=False, ), ClimaCellSensorEntityDescription( @@ -432,7 +432,7 @@ def __post_init__(self) -> None: name="Particulate Matter < 10 μm", unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399 ** 3, + metric_conversion=3.2808399**3, is_metric_check=False, ), ClimaCellSensorEntityDescription( diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index bb7dea841e49a..4928d92447e15 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/climacell", "requirements": ["pyclimacell==0.18.2"], "codeowners": ["@raman325"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyclimacell"] } diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json index 85dc0e7f4fe57..1f9956a75fa3a 100644 --- a/homeassistant/components/climacell/translations/el.json +++ b/homeassistant/components/climacell/translations/el.json @@ -1,6 +1,8 @@ { "config": { "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", "rate_limited": "\u0391\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae \u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\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" }, diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json index deb60db2004c2..17c63089dbf19 100644 --- a/homeassistant/components/climacell/translations/es-419.json +++ b/homeassistant/components/climacell/translations/es-419.json @@ -17,8 +17,10 @@ "data": { "timestep": "Min. entre pron\u00f3sticos de NowCast" }, - "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos." + "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar opciones de ClimaCell" } } - } + }, + "title": "ClimaCell" } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 89e3f43be56db..38182d67c4acd 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -26,7 +26,7 @@ "timestep": "Min. Entre les pr\u00e9visions NowCast" }, "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00abnowcast\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", - "title": "Mettre \u00e0 jour les options de ClimaCell" + "title": "Mettre \u00e0 jour les options ClimaCell" } } }, diff --git a/homeassistant/components/climacell/translations/pt-BR.json b/homeassistant/components/climacell/translations/pt-BR.json new file mode 100644 index 0000000000000..54de15d1f7fd0 --- /dev/null +++ b/homeassistant/components/climacell/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "rate_limited": "Taxa atualmente limitada, tente novamente mais tarde.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "api_version": "Vers\u00e3o da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Se Latitude e Longitude n\u00e3o forem fornecidos, os valores padr\u00f5es na configura\u00e7\u00e3o do Home Assistant ser\u00e3o usados. Uma entidade ser\u00e1 criada para cada tipo de previs\u00e3o, mas apenas as selecionadas ser\u00e3o habilitadas por padr\u00e3o." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "M\u00ednimo entre previs\u00f5es NowCast" + }, + "description": "Se voc\u00ea optar por ativar a entidade de previs\u00e3o `nowcast`, poder\u00e1 configurar o n\u00famero de minutos entre cada previs\u00e3o. O n\u00famero de previs\u00f5es fornecidas depende do n\u00famero de minutos escolhidos entre as previs\u00f5es.", + "title": "Atualizar as op\u00e7\u00f5es do ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.es-419.json b/homeassistant/components/climacell/translations/sensor.es-419.json new file mode 100644 index 0000000000000..127177e84b443 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.es-419.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", + "very_unhealthy": "Muy poco saludable" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio", + "none": "Ninguno", + "very_high": "Muy alto", + "very_low": "Muy bajo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Lluvia helada", + "ice_pellets": "Gr\u00e1nulos de hielo", + "none": "Ninguno", + "rain": "Lluvia", + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json index 8f5fbb244ff78..95625c2b8da68 100644 --- a/homeassistant/components/climacell/translations/sensor.fr.json +++ b/homeassistant/components/climacell/translations/sensor.fr.json @@ -3,12 +3,16 @@ "climacell__health_concern": { "good": "Bon", "hazardous": "Hasardeux", - "moderate": "Mod\u00e9r\u00e9" + "moderate": "Mod\u00e9r\u00e9", + "unhealthy": "Mauvais pour la sant\u00e9", + "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s mauvais pour la sant\u00e9" }, "climacell__pollen_index": { "high": "Haut", "low": "Faible", "medium": "Moyen", + "none": "Aucun", "very_high": "Tr\u00e8s \u00e9lev\u00e9", "very_low": "Tr\u00e8s faible" }, diff --git a/homeassistant/components/climacell/translations/sensor.id.json b/homeassistant/components/climacell/translations/sensor.id.json new file mode 100644 index 0000000000000..37ac0f7d87628 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.id.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bagus", + "hazardous": "Berbahaya", + "moderate": "Sedang", + "unhealthy": "Tidak Sehat", + "unhealthy_for_sensitive_groups": "Tidak Sehat untuk Kelompok Sensitif", + "very_unhealthy": "Sangat Tidak Sehat" + }, + "climacell__pollen_index": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "none": "Tidak Ada", + "very_high": "Sangat Tinggi", + "very_low": "Sangat Rendah" + }, + "climacell__precipitation_type": { + "freezing_rain": "Hujan Beku", + "ice_pellets": "Hujan Es", + "none": "Tidak Ada", + "rain": "Hujan", + "snow": "Salju" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.lv.json b/homeassistant/components/climacell/translations/sensor.lv.json new file mode 100644 index 0000000000000..a0010b4e4a897 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.lv.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Labs", + "hazardous": "B\u012bstams", + "moderate": "M\u0113rens", + "unhealthy": "Nevesel\u012bgs", + "unhealthy_for_sensitive_groups": "Nevesel\u012bgs jut\u012bg\u0101m grup\u0101m", + "very_unhealthy": "\u013boti nevesel\u012bgs" + }, + "climacell__pollen_index": { + "high": "Augsts", + "low": "Zems", + "medium": "Vid\u0113js", + "none": "Nav", + "very_high": "\u013boti augsts", + "very_low": "\u013boti zems" + }, + "climacell__precipitation_type": { + "freezing_rain": "Sasalsto\u0161s lietus", + "ice_pellets": "Krusa", + "none": "Nav", + "rain": "Lietus", + "snow": "Sniegs" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json index bd47b20aa7d23..c457988681bbb 100644 --- a/homeassistant/components/climacell/translations/sensor.nl.json +++ b/homeassistant/components/climacell/translations/sensor.nl.json @@ -1,13 +1,27 @@ { "state": { "climacell__health_concern": { + "good": "Goed", "hazardous": "Gevaarlijk", + "moderate": "Gematigd", "unhealthy": "Ongezond", "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", - "very_unhealthy": "Heel ongezond" + "very_unhealthy": "Zeer ongezond" }, "climacell__pollen_index": { - "none": "Geen" + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "none": "Geen", + "very_high": "Zeer Hoog", + "very_low": "Zeer Laag" + }, + "climacell__precipitation_type": { + "freezing_rain": "IJzel", + "ice_pellets": "Hagel", + "none": "Geen", + "rain": "Regen", + "snow": "Sneeuw" } } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.pt-BR.json b/homeassistant/components/climacell/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..eb3814331b90c --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt-BR.json @@ -0,0 +1,27 @@ +{ + "state": { + "climacell__health_concern": { + "good": "Bom", + "hazardous": "Perigosos", + "moderate": "Moderado", + "unhealthy": "Pouco saud\u00e1vel", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", + "very_unhealthy": "Muito prejudicial \u00e0 sa\u00fade" + }, + "climacell__pollen_index": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio", + "none": "Nenhum", + "very_high": "Muito alto", + "very_low": "Muito baixo" + }, + "climacell__precipitation_type": { + "freezing_rain": "Chuva congelante", + "ice_pellets": "Granizo", + "none": "Nenhum", + "rain": "Chuva", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json new file mode 100644 index 0000000000000..8e0bc629a1328 --- /dev/null +++ b/homeassistant/components/climacell/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/el.json b/homeassistant/components/climate/translations/el.json index 90e25c258f12e..a7aa538641749 100644 --- a/homeassistant/components/climate/translations/el.json +++ b/homeassistant/components/climate/translations/el.json @@ -4,6 +4,10 @@ "set_hvac_mode": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 HVAC \u03c3\u03c4\u03bf {entity_name}", "set_preset_mode": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c4\u03bf {entity_name}" }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 HVAC", + "is_preset_mode": "{entity_name} \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c0\u03c1\u03bf\u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1" + }, "trigger_type": { "current_humidity_changed": "\u0397 \u03bc\u03b5\u03c4\u03c1\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5", "current_temperature_changed": "\u0397 \u03bc\u03b5\u03c4\u03c1\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5", diff --git a/homeassistant/components/climate/translations/pt-BR.json b/homeassistant/components/climate/translations/pt-BR.json index e920caf2a870f..fc745aaef3962 100644 --- a/homeassistant/components/climate/translations/pt-BR.json +++ b/homeassistant/components/climate/translations/pt-BR.json @@ -1,10 +1,25 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "Alterar o modo HVAC em {entity_name}", + "set_preset_mode": "Alterar predefini\u00e7\u00e3o em {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} est\u00e1 definido para um modo HVAC espec\u00edfico", + "is_preset_mode": "{entity_name} est\u00e1 definido para um modo predefinido espec\u00edfico" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} umidade medida alterada", + "current_temperature_changed": "{entity_name} temperatura medida alterada", + "hvac_mode_changed": "{entity_name} modo HVAC alterado" + } + }, "state": { "_": { "auto": "Autom\u00e1tico", "cool": "Frio", "dry": "Seco", - "fan_only": "Apenas ventilador", + "fan_only": "Apenas ventilar", "heat": "Quente", "heat_cool": "Quente/Frio", "off": "Desligado" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 7353ba6fd2187..360e726c89ef6 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,5 +1,9 @@ """Component to integrate the Home Assistant cloud.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable, Callable +from enum import Enum from hass_nabucasa import Cloud import voluptuous as vol @@ -18,6 +22,10 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest @@ -52,6 +60,8 @@ SERVICE_REMOTE_CONNECT = "remote_connect" SERVICE_REMOTE_DISCONNECT = "remote_disconnect" +SIGNAL_CLOUD_CONNECTION_STATE = "CLOUD_CONNECTION_STATE" + ALEXA_ENTITY_SCHEMA = vol.Schema( { @@ -118,6 +128,13 @@ class CloudNotConnected(CloudNotAvailable): """Raised when an action requires the cloud but it's not connected.""" +class CloudConnectionState(Enum): + """Cloud connection state.""" + + CLOUD_CONNECTED = "cloud_connected" + CLOUD_DISCONNECTED = "cloud_disconnected" + + @bind_hass @callback def async_is_logged_in(hass: HomeAssistant) -> bool: @@ -135,6 +152,15 @@ def async_is_connected(hass: HomeAssistant) -> bool: return DOMAIN in hass.data and hass.data[DOMAIN].iot.connected +@callback +def async_listen_connection_change( + hass: HomeAssistant, + target: Callable[[CloudConnectionState], Awaitable[None] | None], +) -> Callable[[], None]: + """Notify on connection state changes.""" + return async_dispatcher_connect(hass, SIGNAL_CLOUD_CONNECTION_STATE, target) + + @bind_hass @callback def async_active_subscription(hass: HomeAssistant) -> bool: @@ -252,11 +278,22 @@ async def _on_connect(): Platform.TTS, DOMAIN, {}, config ) + async_dispatcher_send( + hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED + ) + + async def _on_disconnect(): + """Handle cloud disconnect.""" + async_dispatcher_send( + hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED + ) + async def _on_initialized(): """Update preferences.""" await prefs.async_update(remote_domain=cloud.remote.instance_domain) cloud.iot.register_on_connect(_on_connect) + cloud.iot.register_on_disconnect(_on_disconnect) cloud.register_on_initialized(_on_initialized) await cloud.initialize() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 517d6b2bb9b1d..0ea1fc1d3f31d 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -1,5 +1,6 @@ """The HTTP api to control the cloud integration.""" import asyncio +import dataclasses from functools import wraps from http import HTTPStatus import logging @@ -434,13 +435,22 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud): else: certificate = None + if cloud.iot.last_disconnect_reason: + cloud_last_disconnect_reason = dataclasses.asdict( + cloud.iot.last_disconnect_reason + ) + else: + cloud_last_disconnect_reason = None + return { "alexa_entities": client.alexa_user_config["filter"].config, "alexa_registered": alexa_config.authorized, "cloud": cloud.iot.state, + "cloud_last_disconnect_reason": cloud_last_disconnect_reason, "email": claims["email"], "google_entities": client.google_user_config["filter"].config, "google_registered": google_config.has_registered_user_agent, + "google_local_connected": google_config.is_local_connected, "logged_in": True, "prefs": client.prefs.as_dict(), "remote_certificate": certificate, diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index b83c4c4cca9b6..d5d0c2c03705e 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,9 +2,10 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.52.0"], + "requirements": ["hass-nabucasa==0.54.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["hass_nabucasa"] } diff --git a/homeassistant/components/cloud/translations/ja.json b/homeassistant/components/cloud/translations/ja.json index 298163c3d41c4..bf0ae52b106b3 100644 --- a/homeassistant/components/cloud/translations/ja.json +++ b/homeassistant/components/cloud/translations/ja.json @@ -6,7 +6,7 @@ "can_reach_cloud": "Home Assistant Cloud\u3078\u306e\u30a2\u30af\u30bb\u30b9", "can_reach_cloud_auth": "\u8a8d\u8a3c\u30b5\u30fc\u30d0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9", "google_enabled": "Google\u6709\u52b9", - "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08", + "logged_in": "\u30ed\u30b0\u30a4\u30f3\u6e08\u307f", "relayer_connected": "\u63a5\u7d9a\u3055\u308c\u305f\u518d\u30ec\u30a4\u30e4\u30fc", "remote_connected": "\u30ea\u30e2\u30fc\u30c8\u63a5\u7d9a", "remote_enabled": "\u30ea\u30e2\u30fc\u30c8\u6709\u52b9", diff --git a/homeassistant/components/cloud/translations/pt-BR.json b/homeassistant/components/cloud/translations/pt-BR.json new file mode 100644 index 0000000000000..7e9a1f71c0659 --- /dev/null +++ b/homeassistant/components/cloud/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa habilitada", + "can_reach_cert_server": "Alcance o servidor de certificados", + "can_reach_cloud": "Alcance a nuvem do Home Assistant", + "can_reach_cloud_auth": "Alcance o servidor de autentica\u00e7\u00e3o", + "google_enabled": "Google ativado", + "logged_in": "Logado", + "relayer_connected": "Relayer Conectado", + "remote_connected": "Conectado remotamente", + "remote_enabled": "Conex\u00e3o remota habilitada", + "remote_server": "Servidor remoto", + "subscription_expiration": "Expira\u00e7\u00e3o da assinatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index ebb9e4b5f6285..73b83c24cce2d 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pycfdns==1.2.2"], "codeowners": ["@ludeeus", "@ctalkington"], "config_flow": true, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pycfdns"] } diff --git a/homeassistant/components/cloudflare/translations/cs.json b/homeassistant/components/cloudflare/translations/cs.json index 8f88377860b55..2f087600355dd 100644 --- a/homeassistant/components/cloudflare/translations/cs.json +++ b/homeassistant/components/cloudflare/translations/cs.json @@ -14,7 +14,8 @@ "step": { "reauth_confirm": { "data": { - "api_token": "API token" + "api_token": "API token", + "description": "Op\u011bt ov\u011b\u0159te sv\u016fj Cloudflare \u00fa\u010det." } }, "records": { @@ -27,7 +28,7 @@ "data": { "api_token": "API token" }, - "description": "Tato integrace vy\u017eaduje API token vytvo\u0159en\u00fd s opravn\u011bn\u00edmi Z\u00f3na:Z\u00f3na:\u010c\u00edst a Z\u00f3na:DNS: Upravit pro v\u0161echny z\u00f3ny ve va\u0161em \u00fa\u010dtu.", + "description": "Tato integrace vy\u017eaduje API token vytvo\u0159en\u00fd s opravn\u011bn\u00edmi Zone:Zone:Read a Zone:DNS:Edit pro v\u0161echny z\u00f3ny ve va\u0161em \u00fa\u010dtu.", "title": "P\u0159ipojen\u00ed ke Cloudflare" }, "zone": { diff --git a/homeassistant/components/cloudflare/translations/el.json b/homeassistant/components/cloudflare/translations/el.json index 345aa94303969..44159809e3c3f 100644 --- a/homeassistant/components/cloudflare/translations/el.json +++ b/homeassistant/components/cloudflare/translations/el.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "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": "\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", + "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_zone": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b6\u03ce\u03bd\u03b7" }, "flow_title": "{name}", "step": { + "reauth_confirm": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API", + "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Cloudflare." + } + }, "records": { "data": { "records": "\u0395\u03b3\u03b3\u03c1\u03b1\u03c6\u03ad\u03c2" @@ -12,6 +25,9 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" }, "user": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bd\u03b1 Token API \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b5 \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 Zone:Zone:Read \u03ba\u03b1\u03b9 Zone:DNS:Edit \u03b3\u03b9\u03b1 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03b6\u03ce\u03bd\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Cloudflare" }, diff --git a/homeassistant/components/cloudflare/translations/es-419.json b/homeassistant/components/cloudflare/translations/es-419.json index 03b49267d1264..561c9efa24be4 100644 --- a/homeassistant/components/cloudflare/translations/es-419.json +++ b/homeassistant/components/cloudflare/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_zone": "Zona inv\u00e1lida" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/cloudflare/translations/pt-BR.json b/homeassistant/components/cloudflare/translations/pt-BR.json new file mode 100644 index 0000000000000..c591abe2b3be1 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_zone": "Zona inv\u00e1lida" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token da API", + "description": "Re-autentique com sua conta Cloudflare." + } + }, + "records": { + "data": { + "records": "Registros" + }, + "title": "Escolha os registros a serem atualizados" + }, + "user": { + "data": { + "api_token": "Token da API" + }, + "description": "Essa integra\u00e7\u00e3o requer um token de API criado com as permiss\u00f5es Zone:Zone:Read e Zone:DNS:Edit para todas as zonas em sua conta.", + "title": "Conecte-se \u00e0 Cloudflare" + }, + "zone": { + "data": { + "zone": "Zona" + }, + "title": "Escolha a Zona para Atualizar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/sk.json b/homeassistant/components/cloudflare/translations/sk.json new file mode 100644 index 0000000000000..4af875cd1ab0f --- /dev/null +++ b/homeassistant/components/cloudflare/translations/sk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "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", + "invalid_zone": "Neplatn\u00e1 z\u00f3na" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "data": { + "api_token": "API token", + "description": "Znovu overte svoj Cloudflare \u00fa\u010det." + } + }, + "records": { + "data": { + "records": "Z\u00e1znamy" + }, + "title": "Vyberte z\u00e1znamy, ktor\u00e9 chcete aktualizova\u0165" + }, + "user": { + "data": { + "api_token": "API token" + }, + "description": "T\u00e1to integr\u00e1cia vy\u017eaduje API token vytvoren\u00fd s opr\u00e1vneniami Zone:Zone:Read a Zone:DNS:Edit pre v\u0161etky z\u00f3ny vo va\u0161om \u00fa\u010dte.", + "title": "Pripoji\u0165 k slu\u017ebe Cloudflare" + }, + "zone": { + "data": { + "zone": "Z\u00f3na" + }, + "title": "Vyberte z\u00f3nu, ktor\u00fa chcete aktualizova\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/uk.json b/homeassistant/components/cloudflare/translations/uk.json index 425ec2733b8f6..a8e383dc7b71d 100644 --- a/homeassistant/components/cloudflare/translations/uk.json +++ b/homeassistant/components/cloudflare/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index 3ee29277296eb..675c2b74d28be 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 7e785af57c165..bf2bb9290fc99 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": ["pycmus==0.1.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pbr", "pycmus"] } diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 1921ae4f57523..2af5c8bcb2ffa 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -7,5 +7,6 @@ ], "codeowners": [], "iot_class": "cloud_polling", - "config_flow": true + "config_flow": true, + "loggers": ["CO2Signal"] } \ No newline at end of file diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 8d691b0e5c9eb..b10cd054ff9f4 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -92,14 +92,15 @@ def __init__( def available(self) -> bool: """Return True if entity is available.""" return ( - super().available - and self.coordinator.data["data"].get(self._description.key) is not None + super().available and self._description.key in self.coordinator.data["data"] ) @property def native_value(self) -> StateType: """Return sensor state.""" - return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] + if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[misc] + return None + return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/co2signal/translations/cs.json b/homeassistant/components/co2signal/translations/cs.json index 954168d1ee21b..e8a60b65e8216 100644 --- a/homeassistant/components/co2signal/translations/cs.json +++ b/homeassistant/components/co2signal/translations/cs.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "api_ratelimit": "P\u0159ekro\u010den limit vol\u00e1n\u00ed API", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { + "api_ratelimit": "P\u0159ekro\u010den limit vol\u00e1n\u00ed API", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, @@ -22,8 +24,10 @@ }, "user": { "data": { - "api_key": "P\u0159\u00edstupov\u00fd token" - } + "api_key": "P\u0159\u00edstupov\u00fd token", + "location": "Z\u00edskat data pro" + }, + "description": "O token m\u016f\u017eete po\u017e\u00e1dat na adrese https://co2signal.com/." } } } diff --git a/homeassistant/components/co2signal/translations/el.json b/homeassistant/components/co2signal/translations/el.json new file mode 100644 index 0000000000000..cab4466b29334 --- /dev/null +++ b/homeassistant/components/co2signal/translations/el.json @@ -0,0 +1,34 @@ +{ + "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", + "api_ratelimit": "\u03a5\u03c0\u03ad\u03c1\u03b2\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bf\u03c1\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 API", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "api_ratelimit": "\u03a5\u03c0\u03ad\u03c1\u03b2\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bf\u03c1\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 API", + "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": { + "coordinates": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + } + }, + "country": { + "data": { + "country_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03ce\u03c1\u03b1\u03c2" + } + }, + "user": { + "data": { + "api_key": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "location": "\u039b\u03ae\u03c8\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b9\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://co2signal.com/ \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/es-419.json b/homeassistant/components/co2signal/translations/es-419.json index 023c867ee9b82..691c4e2a3501d 100644 --- a/homeassistant/components/co2signal/translations/es-419.json +++ b/homeassistant/components/co2signal/translations/es-419.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API" + }, + "error": { + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API" + }, "step": { "country": { "data": { @@ -9,7 +15,8 @@ "user": { "data": { "location": "Obtener datos para" - } + }, + "description": "Visite https://co2signal.com/ para solicitar un token." } } } diff --git a/homeassistant/components/co2signal/translations/pt-BR.json b/homeassistant/components/co2signal/translations/pt-BR.json new file mode 100644 index 0000000000000..d40c11d57dc36 --- /dev/null +++ b/homeassistant/components/co2signal/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "api_ratelimit": "Limite de taxa da API excedido", + "unknown": "Erro inesperado" + }, + "error": { + "api_ratelimit": "Limite de taxa da API excedido", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "country": { + "data": { + "country_code": "C\u00f3digo do pa\u00eds" + } + }, + "user": { + "data": { + "api_key": "Token de acesso", + "location": "Obter dados para" + }, + "description": "Acesse https://co2signal.com/ para solicitar um token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/sk.json b/homeassistant/components/co2signal/translations/sk.json new file mode 100644 index 0000000000000..915531f8a35f7 --- /dev/null +++ b/homeassistant/components/co2signal/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "api_ratelimit": "Prekro\u010den\u00fd limit API", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "api_ratelimit": "Prekro\u010den\u00fd limit API", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + }, + "country": { + "data": { + "country_code": "K\u00f3d krajiny" + } + }, + "user": { + "data": { + "api_key": "Pr\u00edstupov\u00fd token", + "location": "Z\u00edska\u0165 \u00fadaje pre" + }, + "description": "Ak chcete po\u017eiada\u0165 o token, nav\u0161t\u00edvte https://co2signal.com/." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 238ff1db87d3c..4ef26a1113021 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -99,7 +99,7 @@ def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData: return instance -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) @@ -113,11 +113,11 @@ async def update_listener(hass, config_entry): for entity in entities: currency = entity.unique_id.split("-")[-1] if "xe" in entity.unique_id and currency not in config_entry.options.get( - CONF_EXCHANGE_RATES + CONF_EXCHANGE_RATES, [] ): registry.async_remove(entity.entity_id) elif "wallet" in entity.unique_id and currency not in config_entry.options.get( - CONF_CURRENCIES + CONF_CURRENCIES, [] ): registry.async_remove(entity.entity_id) diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index aa056409786a0..add24a8fd4110 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -9,5 +9,6 @@ "@tombrien" ], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["coinbase"] } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/bg.json b/homeassistant/components/coinbase/translations/bg.json index 6888f4ddf354e..eb72ab1d10d89 100644 --- a/homeassistant/components/coinbase/translations/bg.json +++ b/homeassistant/components/coinbase/translations/bg.json @@ -18,6 +18,8 @@ }, "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.", + "exchange_rate_unavailable": "\u0415\u0434\u0438\u043d \u0438\u043b\u0438 \u043f\u043e\u0432\u0435\u0447\u0435 \u043e\u0442 \u0437\u0430\u044f\u0432\u0435\u043d\u0438\u0442\u0435 \u043e\u0431\u043c\u0435\u043d\u043d\u0438 \u043a\u0443\u0440\u0441\u043e\u0432\u0435 \u043d\u0435 \u0441\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0442 \u043e\u0442 Coinbase.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } } diff --git a/homeassistant/components/coinbase/translations/el.json b/homeassistant/components/coinbase/translations/el.json index c7a90c580ea5f..196aaad864370 100644 --- a/homeassistant/components/coinbase/translations/el.json +++ b/homeassistant/components/coinbase/translations/el.json @@ -1,13 +1,45 @@ { "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": { - "invalid_auth_secret": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 API \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase \u03bb\u03cc\u03b3\u03c9 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd API." + "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_auth_key": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 API \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase \u03bb\u03cc\u03b3\u03c9 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API.", + "invalid_auth_secret": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 API \u03b1\u03c0\u03bf\u03c1\u03c1\u03af\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase \u03bb\u03cc\u03b3\u03c9 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd API.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "api_token": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc API", + "currencies": "\u039d\u03bf\u03bc\u03af\u03c3\u03bc\u03b1\u03c4\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03af\u03c0\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", + "exchange_rates": "\u03a3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API \u03c3\u03b1\u03c2, \u03cc\u03c0\u03c9\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", + "title": "\u0392\u03b1\u03c3\u03b9\u03ba\u03ad\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 API \u03c4\u03bf\u03c5 Coinbase" + } } }, "options": { "error": { "currency_unavailable": "\u0388\u03bd\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03bd\u03bf\u03bc\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf API \u03c4\u03b7\u03c2 Coinbase.", - "exchange_rate_unavailable": "\u039c\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase." + "currency_unavaliable": "\u0388\u03bd\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03bd\u03bf\u03bc\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Coinbase API \u03c3\u03b1\u03c2.", + "exchange_rate_unavailable": "\u039c\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", + "exchange_rate_unavaliable": "\u039c\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "\u03a5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03c0\u03bf\u03c1\u03c4\u03bf\u03c6\u03bf\u03bb\u03b9\u03bf\u03cd \u03c0\u03c1\u03bf\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac.", + "exchange_base": "\u0392\u03b1\u03c3\u03b9\u03ba\u03cc \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ce\u03bd \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03b9\u03ce\u03bd.", + "exchange_rate_currencies": "\u03a3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac." + }, + "description": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Coinbase" + } } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/es-419.json b/homeassistant/components/coinbase/translations/es-419.json index 12acea8a7df5f..c5bc63ee29ac0 100644 --- a/homeassistant/components/coinbase/translations/es-419.json +++ b/homeassistant/components/coinbase/translations/es-419.json @@ -1,14 +1,27 @@ { "config": { + "error": { + "invalid_auth_key": "Credenciales de API rechazadas por Coinbase debido a una clave de API no v\u00e1lida.", + "invalid_auth_secret": "Credenciales de API rechazadas por Coinbase debido a un secreto de API no v\u00e1lido." + }, "step": { "user": { "data": { "api_token": "Secreto de la API", + "currencies": "Monedas del saldo de la cuenta", "exchange_rates": "Tipos de cambio" }, "description": "Ingrese los detalles de su clave API proporcionada por Coinbase.", "title": "Detalles clave de la API de Coinbase" } } + }, + "options": { + "error": { + "currency_unavailable": "Su API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "currency_unavaliable": "Su API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "exchange_rate_unavailable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", + "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados." + } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json index 3feddeb7cd0de..74ee4c61f9797 100644 --- a/homeassistant/components/coinbase/translations/fr.json +++ b/homeassistant/components/coinbase/translations/fr.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", "currency_unavaliable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", + "exchange_rate_unavailable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", "exchange_rate_unavaliable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index 598c1a1c7281a..1d431ffd2fdd0 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", "currency_unavaliable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", + "exchange_rate_unavailable": "Satu atau beberapa nilai tukar yang diminta tidak disediakan oleh Coinbase.", "exchange_rate_unavaliable": "Satu atau beberapa nilai tukar yang diminta tidak disediakan oleh Coinbase.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index e0bd3a5684216..ecc0d93e747cb 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "Uno o pi\u00f9 dei saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", "currency_unavaliable": "Uno o pi\u00f9 saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", + "exchange_rate_unavailable": "Uno o pi\u00f9 dei tassi di cambio richiesti non sono forniti da Coinbase.", "exchange_rate_unavaliable": "Uno o pi\u00f9 dei tassi di cambio richiesti non sono forniti da Coinbase.", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index 2eebb52601561..fc2068cb01a2a 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "Een of meer van de gevraagde valutabalansen wordt niet geleverd door uw Coinbase API.", "currency_unavaliable": "Een of meer van de gevraagde valutasaldi worden niet geleverd door uw Coinbase API.", + "exchange_rate_unavailable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase geleverd.", "exchange_rate_unavaliable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase verstrekt.", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index ad1cb5087f149..9a24d984c32e3 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", "currency_unavaliable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", + "exchange_rate_unavailable": "En eller flere av de forespurte valutakursene er ikke levert av Coinbase.", "exchange_rate_unavaliable": "En eller flere av de forespurte valutakursene leveres ikke av Coinbase.", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index 1f01a9d69c9f5..e93e71d9e269d 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -25,7 +25,9 @@ }, "options": { "error": { + "currency_unavailable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", "currency_unavaliable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", + "exchange_rate_unavailable": "Jeden lub wi\u0119cej z \u017c\u0105danych kurs\u00f3w wymiany nie jest dostarczany przez Coinbase.", "exchange_rate_unavaliable": "Jeden lub wi\u0119cej z \u017c\u0105danych kurs\u00f3w wymiany nie jest dostarczany przez Coinbase.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/coinbase/translations/pt-BR.json b/homeassistant/components/coinbase/translations/pt-BR.json new file mode 100644 index 0000000000000..6596135208bc4 --- /dev/null +++ b/homeassistant/components/coinbase/translations/pt-BR.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_auth_key": "Credenciais de API rejeitadas pela Coinbase devido a uma chave de API inv\u00e1lida.", + "invalid_auth_secret": "Credenciais de API rejeitadas pela Coinbase devido a um segredo de API inv\u00e1lido.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "api_token": "Segredo da API", + "currencies": "Moedas do saldo da conta", + "exchange_rates": "Taxas de c\u00e2mbio" + }, + "description": "Por favor, insira os detalhes da sua chave de API conforme fornecido pela Coinbase.", + "title": "Detalhes da chave da API Coinbase" + } + } + }, + "options": { + "error": { + "currency_unavailable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.", + "currency_unavaliable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.", + "exchange_rate_unavailable": "Uma ou mais taxas de c\u00e2mbio solicitadas n\u00e3o s\u00e3o fornecidas pela Coinbase.", + "exchange_rate_unavaliable": "Uma ou mais taxas de c\u00e2mbio solicitadas n\u00e3o s\u00e3o fornecidas pela Coinbase.", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Saldos da carteira a relatar.", + "exchange_base": "Moeda base para sensores de taxa de c\u00e2mbio.", + "exchange_rate_currencies": "Taxas de c\u00e2mbio a informar." + }, + "description": "Ajustar as op\u00e7\u00f5es da Coinbase" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sk.json b/homeassistant/components/coinbase/translations/sk.json new file mode 100644 index 0000000000000..ff85312780312 --- /dev/null +++ b/homeassistant/components/coinbase/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json new file mode 100644 index 0000000000000..5610f0c79fd65 --- /dev/null +++ b/homeassistant/components/coinbase/translations/sv.json @@ -0,0 +1,8 @@ +{ + "options": { + "error": { + "currency_unavailable": "En eller flera av de beg\u00e4rda valutasaldona tillhandah\u00e5lls inte av ditt Coinbase API.", + "exchange_rate_unavailable": "En eller flera av de beg\u00e4rda v\u00e4xelkurserna tillhandah\u00e5lls inte av Coinbase." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index d02c10682e174..907211dbae6b0 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": ["pycomfoconnect==0.4"], "codeowners": ["@michaelarnauts"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pycomfoconnect"] } diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 4f98818d9b316..1bcaaaa60a657 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -1,4 +1,5 @@ """The command_line component.""" +from __future__ import annotations import logging import subprocess @@ -6,7 +7,9 @@ _LOGGER = logging.getLogger(__name__) -def call_shell_with_timeout(command, timeout, *, log_return_code=True): +def call_shell_with_timeout( + command: str, timeout: int, *, log_return_code: bool = True +) -> int: """Run a shell command with a timeout. If log_return_code is set to False, it will not print an error if a non-zero @@ -30,7 +33,7 @@ def call_shell_with_timeout(command, timeout, *, log_return_code=True): return -1 -def check_output_or_log(command, timeout): +def check_output_or_log(command: str, timeout: int) -> str | None: """Run a shell command with a timeout and return the output.""" try: return_value = subprocess.check_output( diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index e7cfe5288d80d..b2c8b29478ab6 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -23,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS @@ -59,14 +60,14 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) - name = config.get(CONF_NAME) - command = config.get(CONF_COMMAND) - payload_off = config.get(CONF_PAYLOAD_OFF) - payload_on = config.get(CONF_PAYLOAD_ON) - device_class = config.get(CONF_DEVICE_CLASS) - value_template = config.get(CONF_VALUE_TEMPLATE) - command_timeout = config.get(CONF_COMMAND_TIMEOUT) - unique_id = config.get(CONF_UNIQUE_ID) + name: str = config[CONF_NAME] + 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) + 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) if value_template is not None: value_template.hass = hass data = CommandSensorData(hass, command, command_timeout) @@ -74,7 +75,6 @@ def setup_platform( add_entities( [ CommandBinarySensor( - hass, data, name, device_class, @@ -93,42 +93,25 @@ class CommandBinarySensor(BinarySensorEntity): def __init__( self, - hass, - data, - name, - device_class, - payload_on, - payload_off, - value_template, - unique_id, - ): + data: CommandSensorData, + name: str, + device_class: str | None, + payload_on: str, + payload_off: str, + value_template: Template | None, + unique_id: str | None, + ) -> None: """Initialize the Command line binary sensor.""" - self._hass = hass self.data = data - self._name = name - self._device_class = device_class - self._state = False + self._attr_name = name + self._attr_device_class = device_class + self._attr_is_on = None self._payload_on = payload_on self._payload_off = payload_off self._value_template = value_template self._attr_unique_id = unique_id - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of the binary sensor.""" - return self._device_class - - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" self.data.update() value = self.data.value @@ -136,6 +119,6 @@ def update(self): if self._value_template is not None: value = self._value_template.render_with_possible_json_value(value, False) if value == self._payload_on: - self._state = True + self._attr_is_on = True elif value == self._payload_off: - self._state = False + self._attr_is_on = False diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index ff00138eba440..321b18437d97e 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -20,6 +21,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import call_shell_with_timeout, check_output_or_log @@ -55,17 +57,16 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) - devices = config.get(CONF_COVERS, {}) + devices: dict[str, Any] = config.get(CONF_COVERS, {}) covers = [] for device_name, device_config in devices.items(): - value_template = device_config.get(CONF_VALUE_TEMPLATE) + value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass covers.append( CommandCover( - hass, device_config.get(CONF_FRIENDLY_NAME, device_name), device_config[CONF_COMMAND_OPEN], device_config[CONF_COMMAND_CLOSE], @@ -89,20 +90,18 @@ class CommandCover(CoverEntity): def __init__( self, - hass, - name, - command_open, - command_close, - command_stop, - command_state, - value_template, - timeout, - unique_id, - ): + name: str, + command_open: str, + command_close: str, + command_stop: str, + command_state: str | None, + value_template: Template | None, + timeout: int, + unique_id: str | None, + ) -> None: """Initialize the cover.""" - self._hass = hass - self._name = name - self._state = None + self._attr_name = name + self._state: int | None = None self._command_open = command_open self._command_close = command_close self._command_stop = command_stop @@ -110,8 +109,9 @@ def __init__( self._value_template = value_template self._timeout = timeout self._attr_unique_id = unique_id + self._attr_should_poll = bool(command_state) - def _move_cover(self, command): + def _move_cover(self, command: str) -> bool: """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) @@ -123,35 +123,29 @@ def _move_cover(self, command): return success @property - def should_poll(self): - """Only poll if we have state command.""" - return self._command_state is not None - - @property - def name(self): - """Return the name of the cover.""" - return self._name - - @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self.current_cover_position is not None: return self.current_cover_position == 0 + return None @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. """ return self._state - def _query_state(self): + def _query_state(self) -> str | None: """Query for the state.""" - _LOGGER.info("Running state value command: %s", self._command_state) - return check_output_or_log(self._command_state, self._timeout) + if self._command_state: + _LOGGER.info("Running state value command: %s", self._command_state) + return check_output_or_log(self._command_state, self._timeout) + if TYPE_CHECKING: + return None - def update(self): + def update(self) -> None: """Update device state.""" if self._command_state: payload = str(self._query_state()) @@ -159,14 +153,14 @@ def update(self): payload = self._value_template.render_with_possible_json_value(payload) self._state = int(payload) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs) -> None: """Open the cover.""" self._move_cover(self._command_open) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs) -> None: """Close the cover.""" self._move_cover(self._command_close) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs) -> None: """Stop the cover.""" self._move_cover(self._command_stop) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 1086c6300c2e7..6f364947775ac 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -1,4 +1,6 @@ """Support for command line notification services.""" +from __future__ import annotations + import logging import subprocess @@ -6,7 +8,9 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.process import kill_subprocess from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT @@ -22,10 +26,14 @@ ) -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> CommandLineNotificationService: """Get the Command Line notification service.""" - command = config[CONF_COMMAND] - timeout = config[CONF_COMMAND_TIMEOUT] + command: str = config[CONF_COMMAND] + timeout: int = config[CONF_COMMAND_TIMEOUT] return CommandLineNotificationService(command, timeout) @@ -33,12 +41,12 @@ def get_service(hass, config, discovery_info=None): class CommandLineNotificationService(BaseNotificationService): """Implement the notification service for the Command Line service.""" - def __init__(self, command, timeout): + def __init__(self, command: str, timeout: int) -> None: """Initialize the service.""" self.command = command self._timeout = timeout - def send_message(self, message="", **kwargs): + def send_message(self, message="", **kwargs) -> None: """Send a message to a command line.""" with subprocess.Popen( self.command, diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 387dacfc8de50..5dbbbf88e5814 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -19,10 +19,10 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import check_output_or_log @@ -59,23 +59,19 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) - name = config.get(CONF_NAME) - command = config.get(CONF_COMMAND) - unit = config.get(CONF_UNIT_OF_MEASUREMENT) - value_template = config.get(CONF_VALUE_TEMPLATE) - command_timeout = config.get(CONF_COMMAND_TIMEOUT) - unique_id = config.get(CONF_UNIQUE_ID) + name: str = config[CONF_NAME] + command: str = config[CONF_COMMAND] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + 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) if value_template is not None: value_template.hass = hass - json_attributes = config.get(CONF_JSON_ATTRIBUTES) + json_attributes: list[str] | None = config.get(CONF_JSON_ATTRIBUTES) data = CommandSensorData(hass, command, command_timeout) add_entities( - [ - CommandSensor( - hass, data, name, unit, value_template, json_attributes, unique_id - ) - ], + [CommandSensor(data, name, unit, value_template, json_attributes, unique_id)], True, ) @@ -85,57 +81,35 @@ class CommandSensor(SensorEntity): def __init__( self, - hass, - data, - name, - unit_of_measurement, - value_template, - json_attributes, - unique_id, - ): + data: CommandSensorData, + name: str, + unit_of_measurement: str | None, + value_template: Template | None, + json_attributes: list[str] | None, + unique_id: str | None, + ) -> None: """Initialize the sensor.""" - self._hass = hass self.data = data - self._attributes = None + self._attr_extra_state_attributes = {} self._json_attributes = json_attributes - self._name = name - self._state = None - self._unit_of_measurement = unit_of_measurement + self._attr_name = name + self._attr_native_value = None + self._attr_native_unit_of_measurement = unit_of_measurement self._value_template = value_template self._attr_unique_id = unique_id - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" self.data.update() value = self.data.value if self._json_attributes: - self._attributes = {} + self._attr_extra_state_attributes = {} if value: try: json_dict = json.loads(value) if isinstance(json_dict, Mapping): - self._attributes = { + self._attr_extra_state_attributes = { k: json_dict[k] for k in self._json_attributes if k in json_dict @@ -150,24 +124,26 @@ def update(self): if value is None: value = STATE_UNKNOWN elif self._value_template is not None: - self._state = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN + self._attr_native_value = ( + self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN + ) ) else: - self._state = value + self._attr_native_value = value class CommandSensorData: """The class for handling the data retrieval.""" - def __init__(self, hass, command, command_timeout): + def __init__(self, hass: HomeAssistant, command: str, command_timeout: int) -> None: """Initialize the data object.""" - self.value = None + self.value: str | None = None self.hass = hass self.command = command self.timeout = command_timeout - def update(self): + def update(self) -> None: """Get the latest data with a shell command.""" command = self.command @@ -177,7 +153,7 @@ def update(self): args_compiled = None else: prog, args = command.split(" ", 1) - args_compiled = template.Template(args, self.hass) + args_compiled = Template(args, self.hass) if args_compiled: try: diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index e65db8afcc8f4..ff0d9b65f9dde 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -24,6 +25,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import call_shell_with_timeout, check_output_or_log @@ -59,22 +61,21 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) - devices = config.get(CONF_SWITCHES, {}) + devices: dict[str, Any] = config.get(CONF_SWITCHES, {}) switches = [] for object_id, device_config in devices.items(): - value_template = device_config.get(CONF_VALUE_TEMPLATE) + value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - icon_template = device_config.get(CONF_ICON_TEMPLATE) + icon_template: Template | None = device_config.get(CONF_ICON_TEMPLATE) if icon_template is not None: icon_template.hass = hass switches.append( CommandSwitch( - hass, object_id, device_config.get(CONF_FRIENDLY_NAME, object_id), device_config[CONF_COMMAND_ON], @@ -99,22 +100,20 @@ class CommandSwitch(SwitchEntity): def __init__( self, - hass, - object_id, - friendly_name, - command_on, - command_off, - command_state, - icon_template, - value_template, - timeout, - unique_id, - ): + object_id: str, + friendly_name: str, + command_on: str, + command_off: str, + command_state: str | None, + icon_template: Template | None, + value_template: Template | None, + timeout: int, + unique_id: str | None, + ) -> None: """Initialize the switch.""" - self._hass = hass self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = friendly_name - self._state = False + self._attr_name = friendly_name + self._attr_is_on = False self._command_on = command_on self._command_off = command_off self._command_state = command_state @@ -122,8 +121,9 @@ def __init__( self._value_template = value_template self._timeout = timeout self._attr_unique_id = unique_id + self._attr_should_poll = bool(command_state) - def _switch(self, command): + def _switch(self, command: str) -> bool: """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) @@ -134,12 +134,12 @@ def _switch(self, command): return success - def _query_state_value(self, command): + def _query_state_value(self, command: str) -> str | None: """Execute state command for return value.""" _LOGGER.info("Running state value command: %s", command) return check_output_or_log(command, self._timeout) - def _query_state_code(self, command): + def _query_state_code(self, command: str) -> bool: """Execute state command for return code.""" _LOGGER.info("Running state code command: %s", command) return ( @@ -147,32 +147,20 @@ def _query_state_code(self, command): ) @property - def should_poll(self): - """Only poll if we have state command.""" - return self._command_state is not None - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @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._command_state is None - def _query_state(self): + def _query_state(self) -> str | int | None: """Query for state.""" - if self._value_template: - return self._query_state_value(self._command_state) - return self._query_state_code(self._command_state) + if self._command_state: + if self._value_template: + return self._query_state_value(self._command_state) + return self._query_state_code(self._command_state) + if TYPE_CHECKING: + return None - def update(self): + def update(self) -> None: """Update device state.""" if self._command_state: payload = str(self._query_state()) @@ -182,16 +170,16 @@ def update(self): ) if self._value_template: payload = self._value_template.render_with_possible_json_value(payload) - self._state = payload.lower() == "true" + self._attr_is_on = payload.lower() == "true" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs) -> None: """Turn the device on.""" if self._switch(self._command_on) and not self._command_state: - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs) -> None: """Turn the device off.""" if self._switch(self._command_off) and not self._command_state: - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index cfcd7fe8d6857..dc7bfae38303c 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": ["concord232==0.15"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["concord232", "stevedore"] } diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index d2f630a8b6de1..15fc6634f5b29 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -60,7 +60,6 @@ async def websocket_delete(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "config/auth/create", @@ -69,6 +68,7 @@ async def websocket_delete(hass, connection, msg): vol.Optional("local_only"): bool, } ) +@websocket_api.async_response async def websocket_create(hass, connection, msg): """Create a user.""" user = await hass.auth.async_create_user( @@ -81,7 +81,6 @@ async def websocket_create(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "config/auth/update", @@ -92,6 +91,7 @@ async def websocket_create(hass, connection, msg): vol.Optional("local_only"): bool, } ) +@websocket_api.async_response async def websocket_update(hass, connection, msg): """Update a user.""" if not (user := await hass.auth.async_get_user(msg.pop("user_id"))): diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 887c0517d05c3..07bdc794128b1 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -88,15 +88,17 @@ async def post(self, request, entry_id): raise Unauthorized(config_entry_id=entry_id, permission="remove") hass = request.app["hass"] + entry = hass.config_entries.async_get_entry(entry_id) + if not entry: + return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND) + assert isinstance(entry, config_entries.ConfigEntry) try: - result = await hass.config_entries.async_reload(entry_id) + await hass.config_entries.async_reload(entry_id) except config_entries.OperationNotAllowed: return self.json_message("Entry cannot be reloaded", HTTPStatus.FORBIDDEN) - except config_entries.UnknownEntry: - return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND) - return self.json({"require_restart": not result}) + return self.json({"require_restart": not entry.state.recoverable}) def _prepare_config_flow_result_json(result, prepare_result_json): @@ -262,7 +264,6 @@ def get_entry( @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "config_entries/update", @@ -272,6 +273,7 @@ def get_entry( vol.Optional("pref_disable_polling"): bool, } ) +@websocket_api.async_response async def config_entry_update(hass, connection, msg): """Update config entry.""" changes = dict(msg) @@ -305,7 +307,6 @@ async def config_entry_update(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "config_entries/disable", @@ -315,10 +316,10 @@ async def config_entry_update(hass, connection, msg): "disabled_by": vol.Any(config_entries.ConfigEntryDisabler.USER.value, None), } ) +@websocket_api.async_response async def config_entry_disable(hass, connection, msg): """Disable config entry.""" - disabled_by = msg["disabled_by"] - if disabled_by is not None: + if (disabled_by := msg["disabled_by"]) is not None: disabled_by = config_entries.ConfigEntryDisabler(disabled_by) result = False @@ -339,10 +340,10 @@ async def config_entry_disable(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( {"type": "config_entries/ignore_flow", "flow_id": str, "title": str} ) +@websocket_api.async_response async def ignore_config_flow(hass, connection, msg): """Ignore a config flow.""" flow = next( @@ -388,6 +389,7 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "source": entry.source, "state": entry.state.value, "supports_options": supports_options, + "supports_remove_device": entry.supports_remove_device, "supports_unload": entry.supports_unload, "pref_disable_new_entities": entry.pref_disable_new_entities, "pref_disable_polling": entry.pref_disable_polling, diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index e9e54a688c4fb..3f665e475f070 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -35,7 +35,6 @@ async def post(self, request): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "config/core/update", @@ -50,6 +49,7 @@ async def post(self, request): vol.Optional("currency"): cv.currency, } ) +@websocket_api.async_response async def websocket_update_config(hass, connection, msg): """Handle update core config command.""" data = dict(msg) @@ -64,8 +64,8 @@ async def websocket_update_config(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({"type": "config/core/detect"}) +@websocket_api.async_response async def websocket_detect_config(hass, connection, msg): """Detect core config.""" session = async_get_clientsession(hass) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 686fffec25243..e811d43d502ee 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,16 +1,12 @@ """HTTP views to interact with the device registry.""" import voluptuous as vol +from homeassistant import loader from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.decorators import ( - async_response, - require_admin, -) -from homeassistant.core import callback -from homeassistant.helpers.device_registry import ( - DeviceEntryDisabler, - async_get_registry, -) +from homeassistant.components.websocket_api.decorators import require_admin +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntryDisabler, async_get WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -39,13 +35,16 @@ async def async_setup(hass): websocket_api.async_register_command( hass, WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE ) + websocket_api.async_register_command( + hass, websocket_remove_config_entry_from_device + ) return True -@async_response -async def websocket_list_devices(hass, connection, msg): +@callback +def websocket_list_devices(hass, connection, msg): """Handle list devices command.""" - registry = await async_get_registry(hass) + registry = async_get(hass) connection.send_message( websocket_api.result_message( msg["id"], [_entry_dict(entry) for entry in registry.devices.values()] @@ -54,10 +53,10 @@ async def websocket_list_devices(hass, connection, msg): @require_admin -@async_response -async def websocket_update_device(hass, connection, msg): +@callback +def websocket_update_device(hass, connection, msg): """Handle update area websocket command.""" - registry = await async_get_registry(hass) + registry = async_get(hass) msg.pop("type") msg_id = msg.pop("id") @@ -70,6 +69,57 @@ async def websocket_update_device(hass, connection, msg): connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry))) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + "type": "config/device_registry/remove_config_entry", + "device_id": str, + "config_entry_id": str, + } +) +@websocket_api.async_response +async def websocket_remove_config_entry_from_device( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Remove config entry from a device.""" + registry = async_get(hass) + config_entry_id = msg["config_entry_id"] + device_id = msg["device_id"] + + if (config_entry := hass.config_entries.async_get_entry(config_entry_id)) is None: + raise HomeAssistantError("Unknown config entry") + + if not config_entry.supports_remove_device: + raise HomeAssistantError("Config entry does not support device removal") + + if (device_entry := registry.async_get(device_id)) is None: + raise HomeAssistantError("Unknown device") + + if config_entry_id not in device_entry.config_entries: + raise HomeAssistantError("Config entry not in device") + + try: + integration = await loader.async_get_integration(hass, config_entry.domain) + component = integration.get_component() + except (ImportError, loader.IntegrationNotFound) as exc: + raise HomeAssistantError("Integration not found") from exc + + if not await component.async_remove_config_entry_device( + hass, config_entry, device_entry + ): + raise HomeAssistantError( + "Failed to remove device entry, rejected by integration" + ) + + entry = registry.async_update_device( + device_id, remove_config_entry_id=config_entry_id + ) + + entry_as_dict = _entry_dict(entry) if entry else None + + connection.send_message(websocket_api.result_message(msg["id"], entry_as_dict)) + + @callback def _entry_dict(entry): """Convert entry to API format.""" diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 26a2d930d187d..f5ffc574b8615 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -4,15 +4,12 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND -from homeassistant.components.websocket_api.decorators import ( - async_response, - require_admin, -) +from homeassistant.components.websocket_api.decorators import require_admin from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import ( - RegistryEntryDisabler, - async_get_registry, +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, ) @@ -25,14 +22,11 @@ async def async_setup(hass): return True -@async_response @websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) -async def websocket_list_entities(hass, connection, msg): - """Handle list registry entries command. - - Async friendly. - """ - registry = await async_get_registry(hass) +@callback +def websocket_list_entities(hass, connection, msg): + """Handle list registry entries command.""" + registry = er.async_get(hass) connection.send_message( websocket_api.result_message( msg["id"], [_entry_dict(entry) for entry in registry.entities.values()] @@ -40,19 +34,19 @@ async def websocket_list_entities(hass, connection, msg): ) -@async_response @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", vol.Required("entity_id"): cv.entity_id, } ) -async def websocket_get_entity(hass, connection, msg): +@callback +def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. Async friendly. """ - registry = await async_get_registry(hass) + registry = er.async_get(hass) if (entry := registry.entities.get(msg["entity_id"])) is None: connection.send_message( @@ -66,7 +60,6 @@ async def websocket_get_entity(hass, connection, msg): @require_admin -@async_response @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/update", @@ -81,17 +74,19 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("disabled_by"): vol.Any( None, vol.All( - vol.Coerce(RegistryEntryDisabler), RegistryEntryDisabler.USER.value + vol.Coerce(er.RegistryEntryDisabler), + er.RegistryEntryDisabler.USER.value, ), ), } ) -async def websocket_update_entity(hass, connection, msg): +@callback +def websocket_update_entity(hass, connection, msg): """Handle update entity websocket command. Async friendly. """ - registry = await async_get_registry(hass) + registry = er.async_get(hass) if msg["entity_id"] not in registry.entities: connection.send_message( @@ -120,7 +115,7 @@ async def websocket_update_entity(hass, connection, msg): if "disabled_by" in msg and msg["disabled_by"] is None: entity = registry.entities[msg["entity_id"]] if entity.device_id: - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(entity.device_id) if device.disabled: connection.send_message( @@ -149,19 +144,19 @@ async def websocket_update_entity(hass, connection, msg): @require_admin -@async_response @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/remove", vol.Required("entity_id"): cv.entity_id, } ) -async def websocket_remove_entity(hass, connection, msg): +@callback +def websocket_remove_entity(hass, connection, msg): """Handle remove entity websocket command. Async friendly. """ - registry = await async_get_registry(hass) + registry = er.async_get(hass) if msg["entity_id"] not in registry.entities: connection.send_message( diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index ee2f51303f27d..48a639e56f650 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -115,7 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update when config_entry options update.""" _LOGGER.debug("Config entry was updated, rerunning setup") await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/control4/manifest.json b/homeassistant/components/control4/manifest.json index 656dd5bc93cf6..b00eef2067f10 100644 --- a/homeassistant/components/control4/manifest.json +++ b/homeassistant/components/control4/manifest.json @@ -10,5 +10,6 @@ } ], "codeowners": ["@lawtancool"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyControl4"] } diff --git a/homeassistant/components/control4/translations/el.json b/homeassistant/components/control4/translations/el.json index a80a1248eb5e9..0c11a8b2d5686 100644 --- a/homeassistant/components/control4/translations/el.json +++ b/homeassistant/components/control4/translations/el.json @@ -1,9 +1,31 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 Control4 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03c3\u03b1\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae." } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/pt-BR.json b/homeassistant/components/control4/translations/pt-BR.json index 931024c0e9613..f6fc6c6ef6247 100644 --- a/homeassistant/components/control4/translations/pt-BR.json +++ b/homeassistant/components/control4/translations/pt-BR.json @@ -1,13 +1,29 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "cannot_connect": "Falha na conex\u00e3o" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "host": "Endere\u00e7o IP", "password": "Senha", "username": "Usu\u00e1rio" + }, + "description": "Por favor, insira os detalhes da sua conta Control4 e o endere\u00e7o IP do seu controlador local." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre atualiza\u00e7\u00f5es" } } } diff --git a/homeassistant/components/control4/translations/sk.json b/homeassistant/components/control4/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/control4/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py index b21b75be9b580..2e931d835a8dd 100644 --- a/homeassistant/components/conversation/util.py +++ b/homeassistant/components/conversation/util.py @@ -24,11 +24,11 @@ def create_matcher(utterance): # Group part if group_match is not None: - pattern.append(fr"(?P<{group_match.groups()[0]}>[\w ]+?)\s*") + pattern.append(rf"(?P<{group_match.groups()[0]}>[\w ]+?)\s*") # Optional part elif optional_match is not None: - pattern.append(fr"(?:{optional_match.groups()[0]} *)?") + pattern.append(rf"(?:{optional_match.groups()[0]} *)?") pattern.append("$") return re.compile("".join(pattern), re.I) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index c032c2620ce6a..a56a97f272efa 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": ["pycoolmasternet-async==0.1.2"], "codeowners": ["@OnFreund"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pycoolmasternet_async"] } diff --git a/homeassistant/components/coolmaster/translations/el.json b/homeassistant/components/coolmaster/translations/el.json index b1621652c95c3..9cdc9fe005449 100644 --- a/homeassistant/components/coolmaster/translations/el.json +++ b/homeassistant/components/coolmaster/translations/el.json @@ -12,6 +12,7 @@ "fan_only": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03bc\u03cc\u03bd\u03bf \u03bc\u03b5 \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1", "heat": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "heat_cool": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2/\u03c8\u03cd\u03be\u03b7\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "off": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" }, "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 CoolMasterNet." diff --git a/homeassistant/components/coolmaster/translations/pt-BR.json b/homeassistant/components/coolmaster/translations/pt-BR.json index bb821341818ed..4bb7d51e4640b 100644 --- a/homeassistant/components/coolmaster/translations/pt-BR.json +++ b/homeassistant/components/coolmaster/translations/pt-BR.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "no_units": "N\u00e3o foi poss\u00edvel encontrar nenhuma unidade HVAC no host CoolMasterNet." + }, "step": { "user": { "data": { @@ -8,9 +12,10 @@ "fan_only": "Suporte apenas o modo ventilador", "heat": "Suporta o modo de aquecimento", "heat_cool": "Suporta o modo de aquecimento/resfriamento autom\u00e1tico", - "host": "Host", + "host": "Nome do host", "off": "Pode ser desligado" - } + }, + "title": "Configure seus detalhes de conex\u00e3o CoolMasterNet." } } } diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index 87410d8b5727b..3e7fc50871927 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/coronavirus", "requirements": ["coronavirus==1.1.1"], "codeowners": ["@home-assistant/core"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["coronavirus"] } diff --git a/homeassistant/components/coronavirus/translations/el.json b/homeassistant/components/coronavirus/translations/el.json index c68ccd9e006a7..eacc8de5fd7f0 100644 --- a/homeassistant/components/coronavirus/translations/el.json +++ b/homeassistant/components/coronavirus/translations/el.json @@ -1,10 +1,15 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "user": { "data": { "country": "\u03a7\u03ce\u03c1\u03b1" - } + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c7\u03ce\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" } } } diff --git a/homeassistant/components/coronavirus/translations/pt-BR.json b/homeassistant/components/coronavirus/translations/pt-BR.json index ab4a49048575f..f20cd39494805 100644 --- a/homeassistant/components/coronavirus/translations/pt-BR.json +++ b/homeassistant/components/coronavirus/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Este pa\u00eds j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/el.json b/homeassistant/components/cover/translations/el.json index 1dcade479c6e3..e02c8e3a97b5e 100644 --- a/homeassistant/components/cover/translations/el.json +++ b/homeassistant/components/cover/translations/el.json @@ -13,7 +13,9 @@ "is_closed": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", "is_closing": "{entity_name} \u03ba\u03bb\u03b5\u03af\u03bd\u03b5\u03b9", "is_open": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", - "is_opening": "{entity_name} \u03b1\u03bd\u03bf\u03af\u03b3\u03b5\u03b9" + "is_opening": "{entity_name} \u03b1\u03bd\u03bf\u03af\u03b3\u03b5\u03b9", + "is_position": "\u0397 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b8\u03ad\u03c3\u03b7 {entity_name} \u03b5\u03af\u03bd\u03b1\u03b9", + "is_tilt_position": "\u0397 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b8\u03ad\u03c3\u03b7 \u03ba\u03bb\u03af\u03c3\u03b7\u03c2 {entity_name} \u03b5\u03af\u03bd\u03b1\u03b9" }, "trigger_type": { "closed": "{entity_name} \u03ad\u03ba\u03bb\u03b5\u03b9\u03c3\u03b5", diff --git a/homeassistant/components/cover/translations/pt-BR.json b/homeassistant/components/cover/translations/pt-BR.json index 3403666dfb94c..81689ea1122f5 100644 --- a/homeassistant/components/cover/translations/pt-BR.json +++ b/homeassistant/components/cover/translations/pt-BR.json @@ -1,4 +1,31 @@ { + "device_automation": { + "action_type": { + "close": "Fechar {entity_name}", + "close_tilt": "Fechar inclina\u00e7\u00e3o de {entity_name}", + "open": "Abra {entity_name}", + "open_tilt": "Abrir inclina\u00e7\u00e3o de {entity_name}", + "set_position": "Definir a posi\u00e7\u00e3o de {entity_name}", + "set_tilt_position": "Definir a posi\u00e7\u00e3o de inclina\u00e7\u00e3o de {entity_name}", + "stop": "Parar {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} est\u00e1 fechado", + "is_closing": "{entity_name} est\u00e1 fechando", + "is_open": "{entity_name} est\u00e1 aberto", + "is_opening": "{entity_name} est\u00e1 abrindo", + "is_position": "A posi\u00e7\u00e3o atual de {entity_name}", + "is_tilt_position": "A posi\u00e7\u00e3o de inclina\u00e7\u00e3o atual de {entity_name}" + }, + "trigger_type": { + "closed": "{entity_name} for fechado", + "closing": "{entity_name} estiver fechando", + "opened": "{entity_name} for aberto", + "opening": "{entity_name} estiver abrindo", + "position": "houver mudan\u00e7a de posi\u00e7\u00e3o de {entity_name}", + "tilt_position": "houver mudan\u00e7a na posi\u00e7\u00e3o de inclina\u00e7\u00e3o de {entity_name}" + } + }, "state": { "_": { "closed": "Fechado", diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 99b2383454955..8d21b365ad674 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -75,7 +75,7 @@ def update(self) -> None: info = cpuinfo.get_cpu_info() if info and HZ_ACTUAL in info: - self._attr_native_value = round(float(info[HZ_ACTUAL][0]) / 10 ** 9, 2) + self._attr_native_value = round(float(info[HZ_ACTUAL][0]) / 10**9, 2) else: self._attr_native_value = None @@ -86,5 +86,5 @@ def update(self) -> None: } if HZ_ADVERTISED in info: self._attr_extra_state_attributes[ATTR_HZ] = round( - info[HZ_ADVERTISED][0] / 10 ** 9, 2 + info[HZ_ADVERTISED][0] / 10**9, 2 ) diff --git a/homeassistant/components/cpuspeed/translations/bg.json b/homeassistant/components/cpuspeed/translations/bg.json index 500891c40a68d..df41c1d7b0ac7 100644 --- a/homeassistant/components/cpuspeed/translations/bg.json +++ b/homeassistant/components/cpuspeed/translations/bg.json @@ -3,6 +3,13 @@ "abort": { "alread_configured": "\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.", "already_configured": "\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." + }, + "step": { + "user": { + "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?", + "title": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 CPU" + } } - } + }, + "title": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 CPU" } \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/el.json b/homeassistant/components/cpuspeed/translations/el.json index ad0e081b16fb0..4c3541e2640bf 100644 --- a/homeassistant/components/cpuspeed/translations/el.json +++ b/homeassistant/components/cpuspeed/translations/el.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "alread_configured": "\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.", + "already_configured": "\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.", "not_compatible": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd CPU, \u03b1\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bc\u03b2\u03b1\u03c4\u03ae \u03bc\u03b5 \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03ac \u03c3\u03b1\u03c2" }, "step": { "user": { + "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;", "title": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 CPU" } } diff --git a/homeassistant/components/cpuspeed/translations/es-419.json b/homeassistant/components/cpuspeed/translations/es-419.json new file mode 100644 index 0000000000000..57c70ac4094cd --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con su sistema" + }, + "step": { + "user": { + "title": "Velocidad de la CPU" + } + } + }, + "title": "Velocidad de la CPU" +} \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/id.json b/homeassistant/components/cpuspeed/translations/id.json index 8046606a6557c..ab3b16da4e903 100644 --- a/homeassistant/components/cpuspeed/translations/id.json +++ b/homeassistant/components/cpuspeed/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "alread_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", - "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "not_compatible": "Tidak dapat mendapatkan informasi CPU, integrasi ini tidak kompatibel dengan sistem Anda" }, "step": { "user": { diff --git a/homeassistant/components/cpuspeed/translations/pt-BR.json b/homeassistant/components/cpuspeed/translations/pt-BR.json new file mode 100644 index 0000000000000..f39ce9b4c9ae7 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "alread_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "already_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "not_compatible": "N\u00e3o \u00e9 poss\u00edvel obter informa\u00e7\u00f5es da CPU, esta integra\u00e7\u00e3o n\u00e3o \u00e9 compat\u00edvel com seu sistema" + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?", + "title": "Velocidade da CPU" + } + } + }, + "title": "Velocidade da CPU" +} \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/zh-Hans.json b/homeassistant/components/cpuspeed/translations/zh-Hans.json index 327a580fc13c5..41130cbdd39fc 100644 --- a/homeassistant/components/cpuspeed/translations/zh-Hans.json +++ b/homeassistant/components/cpuspeed/translations/zh-Hans.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "alread_configured": "\u5f53\u524d\u96c6\u6210\u5df2\u88ab\u914d\u7f6e\uff0c\u4ec5\u80fd\u53ea\u6709\u4e00\u4e2a\u914d\u7f6e", + "already_configured": "\u5f53\u524d\u96c6\u6210\u5df2\u88ab\u914d\u7f6e\uff0c\u4ec5\u80fd\u53ea\u6709\u4e00\u4e2a\u914d\u7f6e", + "not_compatible": "\u65e0\u6cd5\u83b7\u53d6 CPU \u4fe1\u606f\uff0c\u8be5\u96c6\u6210\u4e0e\u60a8\u7684\u7cfb\u7edf\u4e0d\u517c\u5bb9" + }, "step": { "user": { + "description": "\u8bf7\u95ee\u60a8\u662f\u5426\u8981\u5f00\u59cb\u914d\u7f6e\uff1f", "title": "CPU \u901f\u5ea6" } } diff --git a/homeassistant/components/cpuspeed/translations/zh-Hant.json b/homeassistant/components/cpuspeed/translations/zh-Hant.json index 5563dadabc70d..4885aead70ac8 100644 --- a/homeassistant/components/cpuspeed/translations/zh-Hant.json +++ b/homeassistant/components/cpuspeed/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "alread_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", - "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "alread_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "already_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "not_compatible": "\u7121\u6cd5\u53d6\u5f97 CPU \u8cc7\u8a0a\uff0c\u9019\u500b\u63d2\u4ef6\u8207\u4f60\u7684\u7cfb\u7d71\u4e0d\u76f8\u5bb9" }, "step": { diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index 758721d5f71bf..786f54ad6363a 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -11,5 +11,6 @@ ], "codeowners": ["@Crownstone", "@RicArch97"], "after_dependencies": ["usb"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["crownstone_cloud", "crownstone_core", "crownstone_sse", "crownstone_uart"] } diff --git a/homeassistant/components/crownstone/translations/cs.json b/homeassistant/components/crownstone/translations/cs.json index f1e209b21d8f2..1f14cdd8eff14 100644 --- a/homeassistant/components/crownstone/translations/cs.json +++ b/homeassistant/components/crownstone/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "\u00da\u010det je ji\u017e nastaven" }, "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "usb_manual_config": { diff --git a/homeassistant/components/crownstone/translations/el.json b/homeassistant/components/crownstone/translations/el.json new file mode 100644 index 0000000000000..81cfbd9ad390d --- /dev/null +++ b/homeassistant/components/crownstone/translations/el.json @@ -0,0 +1,96 @@ +{ + "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", + "usb_setup_complete": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB.", + "usb_setup_unsuccessful": "\u0397 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB \u03ae\u03c4\u03b1\u03bd \u03b1\u03bd\u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2." + }, + "error": { + "account_not_verified": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 email \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Crownstone.", + "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": { + "usb_config": { + "data": { + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 dongle USB \u03c4\u03bf\u03c5 Crownstone \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"Don't use USB\" (\u039c\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 USB), \u03b1\u03bd \u03b4\u03b5\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 dongle USB.\n\n\u0391\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 VID 10C4 \u03ba\u03b1\u03b9 PID EA60.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB dongle" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 dongle USB Crownstone.", + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 USB dongle Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "title": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB", + "use_usb_option": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 Crownstone USB dongle \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd" + } + }, + "usb_config": { + "data": { + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 Crownstone USB dongle.\n\n\u0391\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 VID 10C4 \u03ba\u03b1\u03b9 PID EA60.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB dongle" + }, + "usb_config_option": { + "data": { + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 Crownstone USB dongle.\n\n\u0391\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 VID 10C4 \u03ba\u03b1\u03b9 PID EA60.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB dongle" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 dongle USB Crownstone.", + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 USB dongle Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 dongle USB Crownstone.", + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 USB dongle Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/es-419.json b/homeassistant/components/crownstone/translations/es-419.json new file mode 100644 index 0000000000000..1c90430254174 --- /dev/null +++ b/homeassistant/components/crownstone/translations/es-419.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "usb_setup_complete": "Configuraci\u00f3n USB de Crownstone completa.", + "usb_setup_unsuccessful": "La configuraci\u00f3n del USB de Crownstone no tuvo \u00e9xito." + }, + "error": { + "account_not_verified": "Cuenta no verificada. Active su cuenta a trav\u00e9s del correo electr\u00f3nico de activaci\u00f3n de Crownstone." + }, + "step": { + "usb_config": { + "description": "Seleccione el puerto serie del dongle USB de Crownstone o seleccione 'No usar USB' si no desea configurar un dongle USB. \n\n Busque un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" + }, + "usb_manual_config": { + "description": "Ingrese manualmente la ruta de un dongle USB de Crownstone.", + "title": "Ruta manual del dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera de Crownstone" + }, + "description": "Seleccione una esfera Crownstone donde se encuentra el USB.", + "title": "Esfera USB Crownstone" + }, + "user": { + "title": "Cuenta de Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Esfera donde se encuentra el USB", + "use_usb_option": "Utilice un dongle USB de Crownstone para la transmisi\u00f3n local de datos" + } + }, + "usb_config": { + "description": "Seleccione el puerto serie del dongle USB Crownstone. \n\n Busque un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" + }, + "usb_config_option": { + "description": "Seleccione el puerto serie del dongle USB Crownstone. \n\n Busque un dispositivo con VID 10C4 y PID EA60." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pt-BR.json b/homeassistant/components/crownstone/translations/pt-BR.json new file mode 100644 index 0000000000000..df4e446837e77 --- /dev/null +++ b/homeassistant/components/crownstone/translations/pt-BR.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "usb_setup_complete": "Configura\u00e7\u00e3o completa do Crownstone USB.", + "usb_setup_unsuccessful": "A configura\u00e7\u00e3o do USB Crownstone n\u00e3o foi bem-sucedida." + }, + "error": { + "account_not_verified": "Conta n\u00e3o verificada. Por favor, ative sua conta atrav\u00e9s do e-mail de ativa\u00e7\u00e3o da Crownstone.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + }, + "description": "Selecione a porta serial do dongle USB Crownstone ou selecione 'N\u00e3o usar USB' se n\u00e3o quiser configurar um dongle USB. \n\n Procure um dispositivo com VID 10C4 e PID EA60.", + "title": "Configura\u00e7\u00e3o do dongle USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + }, + "description": "Insira manualmente o caminho de um dongle USB Crownstone.", + "title": "Caminho manual do dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera de Crownstone" + }, + "description": "Selecione uma Esfera de Crownstone onde o USB est\u00e1 localizado.", + "title": "Esfera USB Crownstone" + }, + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "title": "Conta Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere onde o USB est\u00e1 localizado", + "use_usb_option": "Use um dongle USB Crownstone para transmiss\u00e3o de dados local" + } + }, + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + }, + "description": "Selecione a porta serial do dongle USB Crownstone. \n\n Procure um dispositivo com VID 10C4 e PID EA60.", + "title": "Configura\u00e7\u00e3o do dongle USB Crownstone" + }, + "usb_config_option": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + }, + "description": "Selecione a porta serial do dongle USB Crownstone. \n\n Procure um dispositivo com VID 10C4 e PID EA60.", + "title": "Configura\u00e7\u00e3o do dongle USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + }, + "description": "Insira manualmente o caminho de um dongle USB Crownstone.", + "title": "Caminho manual do dongle USB Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + }, + "description": "Insira manualmente o caminho de um dongle USB Crownstone.", + "title": "Caminho manual do dongle USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera da Pedra da Coroa" + }, + "description": "Selecione um Crownstone Sphere onde o USB est\u00e1 localizado.", + "title": "Esfera USB Crownstone" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Esfera de Crownstone" + }, + "description": "Selecione um Crownstone Sphere onde o USB est\u00e1 localizado.", + "title": "Esfera USB Crownstone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/sk.json b/homeassistant/components/crownstone/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/crownstone/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 30fecb9c6de40..28bfec1476012 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pydaikin"] } diff --git a/homeassistant/components/daikin/translations/el.json b/homeassistant/components/daikin/translations/el.json new file mode 100644 index 0000000000000..cd7b4866aaebc --- /dev/null +++ b/homeassistant/components/daikin/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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "api_password": "\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, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "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" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 Daikin AC. \n\n \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03c4\u03b1 \u039a\u03bb\u03b5\u03b9\u03b4\u03af API \u03ba\u03b1\u03b9 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b1\u03c0\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 BRP072Cxx \u03ba\u03b1\u03b9 SKYFi \u03b1\u03bd\u03c4\u03af\u03c3\u03c4\u03bf\u03b9\u03c7\u03b1.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/pt-BR.json b/homeassistant/components/daikin/translations/pt-BR.json index 11642b57627e3..1489556e10da6 100644 --- a/homeassistant/components/daikin/translations/pt-BR.json +++ b/homeassistant/components/daikin/translations/pt-BR.json @@ -1,15 +1,23 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falha na conex\u00e3o" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "api_password": "Autentica\u00e7\u00e3o inv\u00e1lida, use a chave de API ou a senha.", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "host": "Host" + "api_key": "Chave da API", + "host": "Nome do host", + "password": "Senha" }, - "description": "Digite o endere\u00e7o IP do seu AC Daikin.", + "description": "Insira Endere\u00e7o IP do seu Daikin AC. \n\nObserve que Chave da API e Senha s\u00e3o usados apenas por dispositivos BRP072Cxx e SKYFi, respectivamente.", "title": "Configurar o AC Daikin" } } diff --git a/homeassistant/components/daikin/translations/sk.json b/homeassistant/components/daikin/translations/sk.json new file mode 100644 index 0000000000000..a122282648160 --- /dev/null +++ b/homeassistant/components/daikin/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "api_password": "Neplatn\u00e9 overenie, pou\u017eite bu\u010f API k\u013e\u00fa\u010d alebo heslo.", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index 6468eea0a273f..29c49b68df57a 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": ["pydanfossair==0.1.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pydanfossair"] } diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index deefcaeb906e7..7afd3002fcc97 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": ["python-forecastio==1.4.0"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["forecastio"] } diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index bd2349798fda3..1397285a6fed6 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": ["datadog==0.15.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["datadog"] } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index f069605d43865..112f29db33324 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,11 +12,14 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.entity_registry as er from .config_flow import get_master_gateway -from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN -from .gateway import DeconzGateway +from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN, PLATFORMS +from .deconz_event import async_setup_events, async_unload_events +from .errors import AuthenticationRequired, CannotConnect +from .gateway import DeconzGateway, get_deconz_session from .services import async_setup_services, async_unload_services @@ -33,17 +36,28 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if not config_entry.options: await async_update_master_gateway(hass, config_entry) - gateway = DeconzGateway(hass, config_entry) - if not await gateway.async_setup(): - return False - - if not hass.data[DOMAIN]: - async_setup_services(hass) + try: + api = await get_deconz_session(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + + gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( + hass, config_entry, api + ) - hass.data[DOMAIN][config_entry.entry_id] = gateway + config_entry.add_update_listener(gateway.async_config_entry_updated) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await async_setup_events(gateway) await gateway.async_update_device_registry() + if len(hass.data[DOMAIN]) == 1: + async_setup_services(hass) + + api.start() + config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) ) @@ -53,7 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload deCONZ config entry.""" - gateway = hass.data[DOMAIN].pop(config_entry.entry_id) + gateway: DeconzGateway = hass.data[DOMAIN].pop(config_entry.entry_id) + async_unload_events(gateway) if not hass.data[DOMAIN]: async_unload_services(hass) @@ -89,9 +104,10 @@ async def async_update_group_unique_id( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Update unique ID entities based on deCONZ groups.""" - if not isinstance(old_unique_id := config_entry.data.get(CONF_GROUP_ID_BASE), str): + if not (group_id_base := config_entry.data.get(CONF_GROUP_ID_BASE)): return + old_unique_id = cast(str, group_id_base) new_unique_id = cast(str, config_entry.unique_id) @callback diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index cda743f0893a3..fd674dc1cba06 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -7,7 +7,6 @@ from pydeconz.sensor import ( Alarm, CarbonMonoxide, - DeconzBinarySensor as PydeconzBinarySensor, DeconzSensor as PydeconzSensor, Fire, GenericFlag, @@ -34,21 +33,21 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DECONZ_BINARY_SENSORS = ( - Alarm, - CarbonMonoxide, - Fire, - GenericFlag, - OpenClose, - Presence, - Vibration, - Water, -) - ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" +PROVIDES_EXTRA_ATTRIBUTES = ( + "alarm", + "carbon_monoxide", + "fire", + "flag", + "open", + "presence", + "vibration", + "water", +) + @dataclass class DeconzBinarySensorDescriptionMixin: @@ -56,7 +55,6 @@ class DeconzBinarySensorDescriptionMixin: suffix: str update_key: str - required_attr: str value_fn: Callable[[PydeconzSensor], bool | None] @@ -69,41 +67,90 @@ class DeconzBinarySensorDescription( ENTITY_DESCRIPTIONS = { - Alarm: BinarySensorEntityDescription( - key="alarm", - device_class=BinarySensorDeviceClass.SAFETY, - ), - CarbonMonoxide: BinarySensorEntityDescription( - key="carbonmonoxide", - device_class=BinarySensorDeviceClass.CO, - ), - Fire: BinarySensorEntityDescription( - key="fire", - device_class=BinarySensorDeviceClass.SMOKE, - ), - OpenClose: BinarySensorEntityDescription( - key="openclose", - device_class=BinarySensorDeviceClass.OPENING, - ), - Presence: BinarySensorEntityDescription( - key="presence", - device_class=BinarySensorDeviceClass.MOTION, - ), - Vibration: BinarySensorEntityDescription( - key="vibration", - device_class=BinarySensorDeviceClass.VIBRATION, - ), - Water: BinarySensorEntityDescription( - key="water", - device_class=BinarySensorDeviceClass.MOISTURE, - ), + Alarm: [ + DeconzBinarySensorDescription( + key="alarm", + value_fn=lambda device: device.alarm, + suffix="", + update_key="alarm", + device_class=BinarySensorDeviceClass.SAFETY, + ) + ], + CarbonMonoxide: [ + DeconzBinarySensorDescription( + key="carbon_monoxide", + value_fn=lambda device: device.carbon_monoxide, + suffix="", + update_key="carbonmonoxide", + device_class=BinarySensorDeviceClass.CO, + ) + ], + Fire: [ + DeconzBinarySensorDescription( + key="fire", + value_fn=lambda device: device.fire, + suffix="", + update_key="fire", + device_class=BinarySensorDeviceClass.SMOKE, + ), + DeconzBinarySensorDescription( + key="in_test_mode", + value_fn=lambda device: device.in_test_mode, + suffix="Test Mode", + update_key="test", + device_class=BinarySensorDeviceClass.SMOKE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ], + GenericFlag: [ + DeconzBinarySensorDescription( + key="flag", + value_fn=lambda device: device.flag, + suffix="", + update_key="flag", + ) + ], + OpenClose: [ + DeconzBinarySensorDescription( + key="open", + value_fn=lambda device: device.open, + suffix="", + update_key="open", + device_class=BinarySensorDeviceClass.OPENING, + ) + ], + Presence: [ + DeconzBinarySensorDescription( + key="presence", + value_fn=lambda device: device.presence, + suffix="", + update_key="presence", + device_class=BinarySensorDeviceClass.MOTION, + ) + ], + Vibration: [ + DeconzBinarySensorDescription( + key="vibration", + value_fn=lambda device: device.vibration, + suffix="", + update_key="vibration", + device_class=BinarySensorDeviceClass.VIBRATION, + ) + ], + Water: [ + DeconzBinarySensorDescription( + key="water", + value_fn=lambda device: device.water, + suffix="", + update_key="water", + device_class=BinarySensorDeviceClass.MOISTURE, + ) + ], } - BINARY_SENSOR_DESCRIPTIONS = [ DeconzBinarySensorDescription( - key="tamper", - required_attr="tampered", + key="tampered", value_fn=lambda device: device.tampered, suffix="Tampered", update_key="tampered", @@ -112,22 +159,12 @@ class DeconzBinarySensorDescription( ), DeconzBinarySensorDescription( key="low_battery", - required_attr="low_battery", value_fn=lambda device: device.low_battery, suffix="Low Battery", update_key="lowbattery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), - DeconzBinarySensorDescription( - key="in_test_mode", - required_attr="in_test_mode", - value_fn=lambda device: device.in_test_mode, - suffix="Test Mode", - update_key="test", - device_class=BinarySensorDeviceClass.SMOKE, - entity_category=EntityCategory.DIAGNOSTIC, - ), ] @@ -146,32 +183,26 @@ def async_add_sensor( | ValuesView[PydeconzSensor] = gateway.api.sensors.values(), ) -> None: """Add binary sensor from deCONZ.""" - entities: list[DeconzBinarySensor | DeconzPropertyBinarySensor] = [] + entities: list[DeconzBinarySensor] = [] for sensor in sensors: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if ( - isinstance(sensor, DECONZ_BINARY_SENSORS) - and sensor.unique_id not in gateway.entities[DOMAIN] + known_entities = set(gateway.entities[DOMAIN]) + for description in ( + ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS ): - entities.append(DeconzBinarySensor(sensor, gateway)) - - known_sensor_entities = set(gateway.entities[DOMAIN]) - for sensor_description in BINARY_SENSOR_DESCRIPTIONS: if ( - not hasattr(sensor, sensor_description.required_attr) - or sensor_description.value_fn(sensor) is None + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None ): continue - new_sensor = DeconzPropertyBinarySensor( - sensor, gateway, sensor_description - ) - if new_sensor.unique_id not in known_sensor_entities: + new_sensor = DeconzBinarySensor(sensor, gateway, description) + if new_sensor.unique_id not in known_entities: entities.append(new_sensor) if entities: @@ -194,30 +225,50 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" TYPE = DOMAIN - _device: PydeconzBinarySensor + _device: PydeconzSensor + entity_description: DeconzBinarySensorDescription - def __init__(self, device: PydeconzBinarySensor, gateway: DeconzGateway) -> None: + def __init__( + self, + device: PydeconzSensor, + gateway: DeconzGateway, + description: DeconzBinarySensorDescription, + ) -> None: """Initialize deCONZ binary sensor.""" + self.entity_description: DeconzBinarySensorDescription = description super().__init__(device, gateway) - if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): - self.entity_description = entity_description + if description.suffix: + self._attr_name = f"{self._device.name} {description.suffix}" + + self._update_keys = {description.update_key, "reachable"} + if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES: + self._update_keys.update({"on", "state"}) + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + if self.entity_description.suffix: + return f"{self.serial}-{self.entity_description.suffix.lower()}" + return super().unique_id @callback def async_update_callback(self) -> None: """Update the sensor's state.""" - keys = {"on", "reachable", "state"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property - def is_on(self) -> bool: - """Return true if sensor is on.""" - return self._device.state # type: ignore[no-any-return] + def is_on(self) -> bool | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._device) @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + return + attr: dict[str, bool | float | int | list | None] = {} if self._device.on is not None: @@ -237,40 +288,3 @@ def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength return attr - - -class DeconzPropertyBinarySensor(DeconzDevice, BinarySensorEntity): - """Representation of a deCONZ Property sensor.""" - - TYPE = DOMAIN - _device: PydeconzSensor - entity_description: DeconzBinarySensorDescription - - def __init__( - self, - device: PydeconzSensor, - gateway: DeconzGateway, - description: DeconzBinarySensorDescription, - ) -> None: - """Initialize deCONZ binary sensor.""" - self.entity_description = description - super().__init__(device, gateway) - - self._attr_name = f"{self._device.name} {description.suffix}" - self._update_keys = {description.update_key, "reachable"} - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self.serial}-{self.entity_description.suffix.lower()}" - - @callback - def async_update_callback(self) -> None: - """Update the sensor's state.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - - @property - def is_on(self) -> bool | None: - """Return the state of the sensor.""" - return self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py new file mode 100644 index 0000000000000..2ad53d8ad63ec --- /dev/null +++ b/homeassistant/components/deconz/button.py @@ -0,0 +1,115 @@ +"""Support for deCONZ buttons.""" + +from __future__ import annotations + +from collections.abc import ValuesView +from dataclasses import dataclass + +from pydeconz.group import Scene as PydeconzScene + +from homeassistant.components.button import ( + DOMAIN, + ButtonEntity, + ButtonEntityDescription, +) +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 EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .deconz_device import DeconzSceneMixin +from .gateway import DeconzGateway, get_gateway_from_config_entry + + +@dataclass +class DeconzButtonDescriptionMixin: + """Required values when describing deCONZ button entities.""" + + suffix: str + button_fn: str + + +@dataclass +class DeconzButtonDescription(ButtonEntityDescription, DeconzButtonDescriptionMixin): + """Class describing deCONZ button entities.""" + + +ENTITY_DESCRIPTIONS = { + PydeconzScene: [ + DeconzButtonDescription( + key="store", + button_fn="store", + suffix="Store Current Scene", + icon="mdi:inbox-arrow-down", + entity_category=EntityCategory.CONFIG, + ) + ] +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the deCONZ button entity.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_scene( + scenes: list[PydeconzScene] + | ValuesView[PydeconzScene] = gateway.api.scenes.values(), + ) -> None: + """Add scene button from deCONZ.""" + entities = [] + + for scene in scenes: + + known_entities = set(gateway.entities[DOMAIN]) + for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, []): + + new_entity = DeconzButton(scene, gateway, description) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) + + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + gateway.signal_new_scene, + async_add_scene, + ) + ) + + async_add_scene() + + +class DeconzButton(DeconzSceneMixin, ButtonEntity): + """Representation of a deCONZ button entity.""" + + TYPE = DOMAIN + + def __init__( + self, + device: PydeconzScene, + gateway: DeconzGateway, + description: DeconzButtonDescription, + ) -> None: + """Initialize deCONZ number entity.""" + self.entity_description: DeconzButtonDescription = description + super().__init__(device, gateway) + + self._attr_name = f"{self._attr_name} {description.suffix}" + + async def async_press(self) -> None: + """Store light states into scene.""" + async_button_fn = getattr(self._device, self.entity_description.button_fn) + await async_button_fn() + + def get_device_identifier(self) -> str: + """Return a unique identifier for this scene.""" + return f"{super().get_device_identifier()}-{self.entity_description.key}" diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 5f6d77a69fda2..ca2e791f9e9fb 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -25,6 +25,7 @@ PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.FAN, diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index bbd4051c177e5..45f57729a6f34 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -1,6 +1,8 @@ """Base class for deCONZ devices.""" from __future__ import annotations +from pydeconz.group import Scene as PydeconzScene + from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -96,3 +98,39 @@ def async_update_callback(self): def available(self): """Return True if device is available.""" return self.gateway.available and self._device.reachable + + +class DeconzSceneMixin(DeconzDevice): + """Representation of a deCONZ scene.""" + + _device: PydeconzScene + + def __init__(self, device, gateway) -> None: + """Set up a scene.""" + super().__init__(device, gateway) + + self._attr_name = device.full_name + self._group_identifier = self.get_parent_identifier() + + def get_device_identifier(self) -> str: + """Describe a unique identifier for this scene.""" + return f"{self.gateway.bridgeid}{self._device.deconz_id}" + + def get_parent_identifier(self) -> str: + """Describe a unique identifier for group this scene belongs to.""" + return f"{self.gateway.bridgeid}-{self._device.group_deconz_id}" + + @property + def available(self) -> bool: + """Return True if scene is available.""" + return self.gateway.available + + @property + def unique_id(self) -> str: + """Return a unique identifier for this scene.""" + return self.get_device_identifier() + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + return DeviceInfo(identifiers={(DECONZ_DOMAIN, self._group_identifier)}) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 8fa0c6133dfdd..3bd0278a27a1b 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -1,13 +1,21 @@ """Representation of a deCONZ gateway.""" + +from __future__ import annotations + import asyncio +from types import MappingProxyType +from typing import Any, cast import async_timeout from pydeconz import DeconzSession, errors, group, light, sensor +from pydeconz.alarm_system import AlarmSystem as DeconzAlarmSystem +from pydeconz.group import Group as DeconzGroup +from pydeconz.light import DeconzLight +from pydeconz.sensor import DeconzSensor -from homeassistant.config_entries import SOURCE_HASSIO +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT -from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( aiohttp_client, device_registry as dr, @@ -29,25 +37,23 @@ LOGGER, PLATFORMS, ) -from .deconz_event import async_setup_events, async_unload_events +from .deconz_event import DeconzAlarmEvent, DeconzEvent from .errors import AuthenticationRequired, CannotConnect -@callback -def get_gateway_from_config_entry(hass, config_entry): - """Return gateway with a matching config entry ID.""" - return hass.data[DECONZ_DOMAIN][config_entry.entry_id] - - class DeconzGateway: """Manages a single deCONZ gateway.""" - def __init__(self, hass, config_entry) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: DeconzSession + ) -> None: """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.api = api - self.api = None + api.add_device_callback = self.async_add_device_callback + api.connection_status_callback = self.async_connection_status_callback self.available = True self.ignore_state_updates = False @@ -66,24 +72,24 @@ def __init__(self, hass, config_entry) -> None: sensor.RESOURCE_TYPE: self.signal_new_sensor, } - self.deconz_ids = {} - self.entities = {} - self.events = [] + self.deconz_ids: dict[str, str] = {} + self.entities: dict[str, set[str]] = {} + self.events: list[DeconzAlarmEvent | DeconzEvent] = [] @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" - return self.config_entry.unique_id + return cast(str, self.config_entry.unique_id) @property def host(self) -> str: """Return the host of the gateway.""" - return self.config_entry.data[CONF_HOST] + return cast(str, self.config_entry.data[CONF_HOST]) @property def master(self) -> bool: """Gateway which is used with deCONZ services without defining id.""" - return self.config_entry.options[CONF_MASTER_GATEWAY] + return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY]) # Options @@ -111,7 +117,7 @@ def option_allow_new_devices(self) -> bool: # Callbacks @callback - def async_connection_status_callback(self, available) -> None: + def async_connection_status_callback(self, available: bool) -> None: """Handle signals of gateway connection status.""" self.available = available self.ignore_state_updates = False @@ -119,7 +125,15 @@ def async_connection_status_callback(self, available) -> None: @callback def async_add_device_callback( - self, resource_type, device=None, force: bool = False + self, + resource_type: str, + device: DeconzAlarmSystem + | DeconzGroup + | DeconzLight + | DeconzSensor + | list[DeconzAlarmSystem | DeconzGroup | DeconzLight | DeconzSensor] + | None = None, + force: bool = False, ) -> None: """Handle event of new device creation in deCONZ.""" if ( @@ -166,34 +180,10 @@ async def async_update_device_registry(self) -> None: via_device=(CONNECTION_NETWORK_MAC, self.api.config.mac), ) - async def async_setup(self) -> bool: - """Set up a deCONZ gateway.""" - try: - self.api = await get_gateway( - self.hass, - self.config_entry.data, - self.async_add_device_callback, - self.async_connection_status_callback, - ) - - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - - await async_setup_events(self) - - self.api.start() - - self.config_entry.add_update_listener(self.async_config_entry_updated) - - return True - @staticmethod - async def async_config_entry_updated(hass, entry) -> None: + async def async_config_entry_updated( + hass: HomeAssistant, entry: ConfigEntry + ) -> None: """Handle signals of config entry being updated. This is a static method because a class method (bound method), can not be used with weak references. @@ -209,7 +199,7 @@ async def async_config_entry_updated(hass, entry) -> None: await gateway.options_updated() - async def options_updated(self): + async def options_updated(self) -> None: """Manage entities affected by config entry options.""" deconz_ids = [] @@ -240,14 +230,14 @@ async def options_updated(self): entity_registry.async_remove(entity_id) @callback - def shutdown(self, event) -> None: + def shutdown(self, event: Event) -> None: """Wrap the call to deconz.close. Used as an argument to EventBus.async_listen_once. """ self.api.close() - async def async_reset(self): + async def async_reset(self) -> bool: """Reset this gateway to default state.""" self.api.async_connection_status_callback = None self.api.close() @@ -256,30 +246,35 @@ async def async_reset(self): self.config_entry, PLATFORMS ) - async_unload_events(self) - self.deconz_ids = {} return True -async def get_gateway( - hass, config, async_add_device_callback, async_connection_status_callback +@callback +def get_gateway_from_config_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> DeconzGateway: + """Return gateway with a matching config entry ID.""" + return cast(DeconzGateway, hass.data[DECONZ_DOMAIN][config_entry.entry_id]) + + +async def get_deconz_session( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> DeconzSession: """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession( + deconz_session = DeconzSession( session, config[CONF_HOST], config[CONF_PORT], config[CONF_API_KEY], - add_device=async_add_device_callback, - connection_status=async_connection_status_callback, ) try: async with async_timeout.timeout(10): - await deconz.refresh_state() - return deconz + await deconz_session.refresh_state() + return deconz_session except errors.Unauthorized as err: LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 5330fdb32261e..e3cf644207998 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -5,7 +5,7 @@ from collections.abc import ValuesView from typing import Any -from pydeconz.group import DeconzGroup as Group +from pydeconz.group import Group from pydeconz.light import ( ALERT_LONG, ALERT_SHORT, diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 94356f95eaffb..bbbafffed7a64 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==86" + "pydeconz==87" ], "ssdp": [ { @@ -15,5 +15,8 @@ "@Kane610" ], "quality_scale": "platinum", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": [ + "pydeconz" + ] } \ No newline at end of file diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index fff70b9f7b5e6..bf138aaef639f 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import ValuesView +from collections.abc import Callable, ValuesView from dataclasses import dataclass -from pydeconz.sensor import PRESENCE_DELAY, Presence +from pydeconz.sensor import PRESENCE_DELAY, DeconzSensor as PydeconzSensor, Presence from homeassistant.components.number import ( DOMAIN, @@ -23,33 +23,30 @@ @dataclass -class DeconzNumberEntityDescriptionBase: +class DeconzNumberDescriptionMixin: """Required values when describing deCONZ number entities.""" - device_property: str suffix: str update_key: str + value_fn: Callable[[PydeconzSensor], bool | None] @dataclass -class DeconzNumberEntityDescription( - NumberEntityDescription, DeconzNumberEntityDescriptionBase -): +class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin): """Class describing deCONZ number entities.""" - entity_category = EntityCategory.CONFIG - ENTITY_DESCRIPTIONS = { Presence: [ - DeconzNumberEntityDescription( + DeconzNumberDescription( key="delay", - device_property="delay", + value_fn=lambda device: device.delay, suffix="Delay", update_key=PRESENCE_DELAY, max_value=65535, min_value=0, step=1, + entity_category=EntityCategory.CONFIG, ) ] } @@ -76,15 +73,18 @@ def async_add_sensor( if sensor.type.startswith("CLIP"): continue - known_number_entities = set(gateway.entities[DOMAIN]) + known_entities = set(gateway.entities[DOMAIN]) for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): - if getattr(sensor, description.device_property) is None: + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - new_number_entity = DeconzNumber(sensor, gateway, description) - if new_number_entity.unique_id not in known_number_entities: - entities.append(new_number_entity) + new_entity = DeconzNumber(sensor, gateway, description) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) if entities: async_add_entities(entities) @@ -112,29 +112,29 @@ def __init__( self, device: Presence, gateway: DeconzGateway, - description: DeconzNumberEntityDescription, + description: DeconzNumberDescription, ) -> None: """Initialize deCONZ number entity.""" - self.entity_description: DeconzNumberEntityDescription = description + self.entity_description: DeconzNumberDescription = description super().__init__(device, gateway) self._attr_name = f"{device.name} {description.suffix}" + self._update_keys = {self.entity_description.update_key, "reachable"} @callback def async_update_callback(self) -> None: """Update the number value.""" - keys = {self.entity_description.update_key, "reachable"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property def value(self) -> float: """Return the value of the sensor property.""" - return getattr(self._device, self.entity_description.device_property) # type: ignore[no-any-return] + return self.entity_description.value_fn(self._device) # type: ignore[no-any-return] async def async_set_value(self, value: float) -> None: """Set sensor config.""" - data = {self.entity_description.device_property: int(value)} + data = {self.entity_description.key: int(value)} await self._device.set_config(**data) @property diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 9fcccc523864b..c188d7faffa94 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -5,7 +5,7 @@ from collections.abc import ValuesView from typing import Any -from pydeconz.group import DeconzScene as PydeconzScene +from pydeconz.group import Scene as PydeconzScene from homeassistant.components.scene import DOMAIN, Scene from homeassistant.config_entries import ConfigEntry @@ -13,7 +13,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .deconz_device import DeconzSceneMixin +from .gateway import get_gateway_from_config_entry async def async_setup_entry( @@ -31,11 +32,14 @@ def async_add_scene( | ValuesView[PydeconzScene] = gateway.api.scenes.values(), ) -> None: """Add scene from deCONZ.""" - entities = [ - DeconzScene(scene, gateway) - for scene in scenes - if scene.deconz_id not in gateway.entities[DOMAIN] - ] + entities = [] + + for scene in scenes: + + known_entities = set(gateway.entities[DOMAIN]) + new_entity = DeconzScene(scene, gateway) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) if entities: async_add_entities(entities) @@ -51,27 +55,11 @@ def async_add_scene( async_add_scene() -class DeconzScene(Scene): +class DeconzScene(DeconzSceneMixin, Scene): """Representation of a deCONZ scene.""" - def __init__(self, scene: PydeconzScene, gateway: DeconzGateway) -> None: - """Set up a scene.""" - self._scene = scene - self.gateway = gateway - - self._attr_name = scene.full_name - - async def async_added_to_hass(self) -> None: - """Subscribe to sensors events.""" - self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id - self.gateway.entities[DOMAIN].add(self._scene.deconz_id) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect scene object when removed.""" - del self.gateway.deconz_ids[self.entity_id] - self.gateway.entities[DOMAIN].remove(self._scene.deconz_id) - self._scene = None + TYPE = DOMAIN async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - await self._scene.recall() + await self._device.recall() diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 5c870ffd93782..b0df644f1bdae 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,10 +3,10 @@ from collections.abc import Callable, ValuesView from dataclasses import dataclass +from datetime import datetime from pydeconz.sensor import ( AirQuality, - Battery, Consumption, Daylight, DeconzSensor as PydeconzSensor, @@ -17,7 +17,6 @@ Pressure, Switch, Temperature, - Thermostat, Time, ) @@ -48,22 +47,21 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +import homeassistant.util.dt as dt_util from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DECONZ_SENSORS = ( - AirQuality, - Consumption, - Daylight, - GenericStatus, - Humidity, - LightLevel, - Power, - Pressure, - Temperature, - Time, +PROVIDES_EXTRA_ATTRIBUTES = ( + "battery", + "consumption", + "status", + "humidity", + "light_level", + "power", + "pressure", + "temperature", ) ATTR_CURRENT = "current" @@ -76,9 +74,7 @@ class DeconzSensorDescriptionMixin: """Required values when describing secondary sensor attributes.""" - suffix: str update_key: str - required_attr: str value_fn: Callable[[PydeconzSensor], float | int | None] @@ -89,78 +85,133 @@ class DeconzSensorDescription( ): """Class describing deCONZ binary sensor entities.""" + suffix: str = "" + ENTITY_DESCRIPTIONS = { - Battery: SensorEntityDescription( + AirQuality: [ + DeconzSensorDescription( + key="air_quality", + value_fn=lambda device: device.air_quality, # type: ignore[no-any-return] + update_key="airquality", + state_class=SensorStateClass.MEASUREMENT, + ), + DeconzSensorDescription( + key="air_quality_ppb", + value_fn=lambda device: device.air_quality_ppb, # type: ignore[no-any-return] + suffix="PPB", + update_key="airqualityppb", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + ), + ], + Consumption: [ + DeconzSensorDescription( + key="consumption", + value_fn=lambda device: device.scaled_consumption, # type: ignore[no-any-return] + update_key="consumption", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ) + ], + Daylight: [ + DeconzSensorDescription( + key="status", + value_fn=lambda device: device.status, # type: ignore[no-any-return] + update_key="status", + icon="mdi:white-balance-sunny", + entity_registry_enabled_default=False, + ) + ], + GenericStatus: [ + DeconzSensorDescription( + key="status", + value_fn=lambda device: device.status, # type: ignore[no-any-return] + update_key="status", + ) + ], + Humidity: [ + DeconzSensorDescription( + key="humidity", + value_fn=lambda device: device.scaled_humidity, # type: ignore[no-any-return] + update_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ) + ], + LightLevel: [ + DeconzSensorDescription( + key="light_level", + value_fn=lambda device: device.scaled_light_level, # type: ignore[no-any-return] + update_key="lightlevel", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + ) + ], + Power: [ + DeconzSensorDescription( + key="power", + value_fn=lambda device: device.power, # type: ignore[no-any-return] + update_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ) + ], + Pressure: [ + DeconzSensorDescription( + key="pressure", + value_fn=lambda device: device.pressure, # type: ignore[no-any-return] + update_key="pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ) + ], + Temperature: [ + DeconzSensorDescription( + key="temperature", + value_fn=lambda device: device.temperature, # type: ignore[no-any-return] + update_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ) + ], + Time: [ + DeconzSensorDescription( + key="last_set", + value_fn=lambda device: device.last_set, # type: ignore[no-any-return] + update_key="lastset", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + ], +} + +SENSOR_DESCRIPTIONS = [ + DeconzSensorDescription( key="battery", + value_fn=lambda device: device.battery, # type: ignore[no-any-return] + suffix="Battery", + update_key="battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, ), - Consumption: SensorEntityDescription( - key="consumption", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ), - Daylight: SensorEntityDescription( - key="daylight", - icon="mdi:white-balance-sunny", - entity_registry_enabled_default=False, - ), - Humidity: SensorEntityDescription( - key="humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), - LightLevel: SensorEntityDescription( - key="lightlevel", - device_class=SensorDeviceClass.ILLUMINANCE, - native_unit_of_measurement=LIGHT_LUX, - ), - Power: SensorEntityDescription( - key="power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ), - Pressure: SensorEntityDescription( - key="pressure", - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PRESSURE_HPA, - ), - Temperature: SensorEntityDescription( - key="temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ), -} - -SENSOR_DESCRIPTIONS = [ DeconzSensorDescription( - key="temperature", - required_attr="secondary_temperature", - value_fn=lambda device: device.secondary_temperature, + key="secondary_temperature", + value_fn=lambda device: device.secondary_temperature, # type: ignore[no-any-return] suffix="Temperature", update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), - DeconzSensorDescription( - key="air_quality_ppb", - required_attr="air_quality_ppb", - value_fn=lambda device: device.air_quality_ppb, - suffix="PPB", - update_key="airqualityppb", - device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - ), ] @@ -185,42 +236,33 @@ def async_add_sensor( Create DeconzBattery if sensor has a battery attribute. Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. """ - entities: list[DeconzBattery | DeconzSensor | DeconzPropertySensor] = [] + entities: list[DeconzSensor] = [] for sensor in sensors: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if sensor.battery is not None: - battery_handler.remove_tracker(sensor) - - known_batteries = set(gateway.entities[DOMAIN]) - new_battery = DeconzBattery(sensor, gateway) - if new_battery.unique_id not in known_batteries: - entities.append(new_battery) - - else: + if sensor.battery is None: battery_handler.create_tracker(sensor) - if ( - isinstance(sensor, DECONZ_SENSORS) - and not isinstance(sensor, Thermostat) - and sensor.unique_id not in gateway.entities[DOMAIN] + known_entities = set(gateway.entities[DOMAIN]) + for description in ( + ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS ): - entities.append(DeconzSensor(sensor, gateway)) - - known_sensor_entities = set(gateway.entities[DOMAIN]) - for sensor_description in SENSOR_DESCRIPTIONS: - if not hasattr( - sensor, sensor_description.required_attr - ) or not sensor_description.value_fn(sensor): + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - new_sensor = DeconzPropertySensor(sensor, gateway, sensor_description) - if new_sensor.unique_id not in known_sensor_entities: - entities.append(new_sensor) + new_entity = DeconzSensor(sensor, gateway, description) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) + + if description.key == "battery": + battery_handler.remove_tracker(sensor) if entities: async_add_entities(entities) @@ -243,30 +285,66 @@ class DeconzSensor(DeconzDevice, SensorEntity): TYPE = DOMAIN _device: PydeconzSensor + entity_description: DeconzSensorDescription - def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: - """Initialize deCONZ binary sensor.""" + def __init__( + self, + device: PydeconzSensor, + gateway: DeconzGateway, + description: DeconzSensorDescription, + ) -> None: + """Initialize deCONZ sensor.""" + self.entity_description = description super().__init__(device, gateway) - if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): - self.entity_description = entity_description + if description.suffix: + self._attr_name = f"{device.name} {description.suffix}" + + self._update_keys = {description.update_key, "reachable"} + if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES: + self._update_keys.update({"on", "state"}) + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + if ( + self.entity_description.key == "battery" + and self._device.manufacturer == "Danfoss" + and self._device.model_id + in [ + "0x8030", + "0x8031", + "0x8034", + "0x8035", + ] + ): + return f"{super().unique_id}-battery" + if self.entity_description.suffix: + return f"{self.serial}-{self.entity_description.suffix.lower()}" + return super().unique_id @callback def async_update_callback(self) -> None: """Update the sensor's state.""" - keys = {"on", "reachable", "state"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - return self._device.state # type: ignore[no-any-return] + if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: + return dt_util.parse_datetime( + self.entity_description.value_fn(self._device) + ) + return self.entity_description.value_fn(self._device) @property def extra_state_attributes(self) -> dict[str, bool | float | int | None]: """Return the state attributes of the sensor.""" - attr = {} + attr: dict[str, bool | float | int | None] = {} + + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + return attr if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -292,93 +370,7 @@ def extra_state_attributes(self) -> dict[str, bool | float | int | None]: attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage - return attr - - -class DeconzPropertySensor(DeconzDevice, SensorEntity): - """Representation of a deCONZ secondary attribute sensor.""" - - TYPE = DOMAIN - _device: PydeconzSensor - entity_description: DeconzSensorDescription - - def __init__( - self, - device: PydeconzSensor, - gateway: DeconzGateway, - description: DeconzSensorDescription, - ) -> None: - """Initialize deCONZ sensor.""" - self.entity_description = description - super().__init__(device, gateway) - - self._attr_name = f"{self._device.name} {description.suffix}" - self._update_keys = {description.update_key, "reachable"} - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self.serial}-{self.entity_description.suffix.lower()}" - - @callback - def async_update_callback(self) -> None: - """Update the sensor's state.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - return self.entity_description.value_fn(self._device) - - -class DeconzBattery(DeconzDevice, SensorEntity): - """Battery class for when a device is only represented as an event.""" - - TYPE = DOMAIN - _device: PydeconzSensor - - def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: - """Initialize deCONZ battery level sensor.""" - super().__init__(device, gateway) - - self.entity_description = ENTITY_DESCRIPTIONS[Battery] - self._attr_name = f"{self._device.name} Battery Level" - - @callback - def async_update_callback(self) -> None: - """Update the battery's state, if needed.""" - keys = {"battery", "reachable"} - if self._device.changed_keys.intersection(keys): - super().async_update_callback() - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device. - - Normally there should only be one battery sensor per device from deCONZ. - With specific Danfoss devices each endpoint can report its own battery state. - """ - if self._device.manufacturer == "Danfoss" and self._device.model_id in [ - "0x8030", - "0x8031", - "0x8034", - "0x8035", - ]: - return f"{super().unique_id}-battery" - return f"{self.serial}-battery" - - @property - def native_value(self) -> StateType: - """Return the state of the battery.""" - return self._device.battery # type: ignore[no-any-return] - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the state attributes of the battery.""" - attr = {} - - if isinstance(self._device, Switch): + elif isinstance(self._device, Switch): for event in self.gateway.events: if self._device == event.device: attr[ATTR_EVENT_ID] = event.event_id diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index aeb528c0ac900..4b840532fa6c6 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,7 +1,5 @@ """deCONZ services.""" -from types import MappingProxyType - from pydeconz.utils import normalize_bridge_id import voluptuous as vol @@ -16,6 +14,7 @@ async_entries_for_config_entry, async_entries_for_device, ) +from homeassistant.util.read_only_dict import ReadOnlyDict from .config_flow import get_master_gateway from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER @@ -111,9 +110,7 @@ def async_unload_services(hass: HomeAssistant) -> None: hass.services.async_remove(DOMAIN, service) -async def async_configure_service( - gateway: DeconzGateway, data: MappingProxyType -) -> None: +async def async_configure_service(gateway: DeconzGateway, data: ReadOnlyDict) -> None: """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 61e5e3b5e96a4..2c3e42adcc898 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -9,7 +9,7 @@ "updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host" }, "error": { - "no_key": "Impossibile ottenere una API key" + "no_key": "Impossibile ottenere una chiave API" }, "flow_title": "{host}", "step": { diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index 450fa7707d15a..03004cae30462 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -2,18 +2,20 @@ "config": { "abort": { "already_configured": "A ponte j\u00e1 est\u00e1 configurada", - "already_in_progress": "Fluxo de configura\u00e7\u00e3o para ponte j\u00e1 est\u00e1 em andamento.", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas", + "no_hardware_available": "Nenhum hardware de r\u00e1dio conectado ao deCONZ", "not_deconz_bridge": "N\u00e3o \u00e9 uma ponte deCONZ", "updated_instance": "Atualiza\u00e7\u00e3o da inst\u00e2ncia deCONZ com novo endere\u00e7o de host" }, "error": { "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" }, + "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on Supervisor {addon} ?", - "title": "Gateway deCONZ Zigbee via add-on Supervisor" + "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on {addon} ?", + "title": "Gateway deCONZ Zigbee via add-on" }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", @@ -21,15 +23,87 @@ }, "manual_input": { "data": { + "host": "Nome do host", "port": "Porta" } + }, + "user": { + "data": { + "host": "Selecione o gateway deCONZ descoberto" + } } } }, "device_automation": { "trigger_subtype": { + "both_buttons": "Ambos os bot\u00f5es", "bottom_buttons": "Bot\u00f5es inferiores", - "top_buttons": "Bot\u00f5es superiores" + "button_1": "Primeiro bot\u00e3o", + "button_2": "Segundo bot\u00e3o", + "button_3": "Terceiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o", + "button_5": "Quinto bot\u00e3o", + "button_6": "Sexto bot\u00e3o", + "button_7": "S\u00e9timo bot\u00e3o", + "button_8": "Oitavo bot\u00e3o", + "close": "Fechar", + "dim_down": "Diminuir a luminosidade", + "dim_up": "Aumentar a luminosidade", + "left": "Esquerdo", + "open": "Aberto", + "right": "Direito", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6", + "top_buttons": "Bot\u00f5es superiores", + "turn_off": "Desligar", + "turn_on": "Ligar" + }, + "trigger_type": { + "remote_awakened": "Dispositivo for despertado", + "remote_button_double_press": "bot\u00e3o \" {subtype} \" clicado duas vezes", + "remote_button_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente", + "remote_button_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", + "remote_button_quadruple_press": "Bot\u00e3o \" {subtype} \" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "Bot\u00e3o \" {subtype} \" qu\u00edntuplo clicado", + "remote_button_rotated": "Bot\u00e3o girado \" {subtype} \"", + "remote_button_rotated_fast": "Bot\u00e3o girado r\u00e1pido \"{subtype}\"", + "remote_button_rotation_stopped": "A rota\u00e7\u00e3o dos bot\u00f5es \"{subtype}\" parou", + "remote_button_short_press": "Bot\u00e3o \" {subtype} \" pressionado", + "remote_button_short_release": "Bot\u00e3o \" {subtype} \" liberados", + "remote_button_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes", + "remote_double_tap": "Dispositivo \"{subtype}\" tocado duas vezes", + "remote_double_tap_any_side": "Dispositivo tocado duas vezes em qualquer lado", + "remote_falling": "Dispositivo em queda livre", + "remote_flip_180_degrees": "Dispositivo invertido 180 graus", + "remote_flip_90_degrees": "Dispositivo invertido 90 graus", + "remote_gyro_activated": "Dispositivo sacudido", + "remote_moved": "Dispositivo movido com \"{subtype}\" para cima", + "remote_moved_any_side": "Dispositivo movido com qualquer lado para cima", + "remote_rotate_from_side_1": "Dispositivo girado de \"lado 1\" para \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo girado de \"lado 2\" para \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo girado de \"lado 3\" para \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo girado de \"lado 4\" para \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo girado de \"lado 5\" para \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" para \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo girado no sentido hor\u00e1rio", + "remote_turned_counter_clockwise": "Dispositivo girado no sentido anti-hor\u00e1rio" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ", + "allow_new_devices": "Permitir a adi\u00e7\u00e3o autom\u00e1tica de novos dispositivos" + }, + "description": "Configure a visibilidade dos tipos de dispositivos deCONZ", + "title": "Op\u00e7\u00f5es deCONZ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/sk.json b/homeassistant/components/deconz/translations/sk.json new file mode 100644 index 0000000000000..8168473947488 --- /dev/null +++ b/homeassistant/components/deconz/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "manual_input": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/zh-Hans.json b/homeassistant/components/deconz/translations/zh-Hans.json index dfe8209fa1c93..11e196e75f20e 100644 --- a/homeassistant/components/deconz/translations/zh-Hans.json +++ b/homeassistant/components/deconz/translations/zh-Hans.json @@ -11,6 +11,11 @@ "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" + }, + "manual_input": { + "data": { + "port": "\u7aef\u53e3" + } } } }, diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index b631467e5e31e..3734339a34bcb 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": ["bluepy==1.3.0", "decora==0.6"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bluepy", "decora"] } diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 4f1f9f059d898..84caf0ad29acb 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -96,6 +96,7 @@ class DecoraWifiLight(LightEntity): def __init__(self, switch): """Initialize the switch.""" self._switch = switch + self._attr_unique_id = switch.serial @property def supported_features(self): diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index 1fd2b1737ad53..35af18a8c3075 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": ["decora_wifi==1.4"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["decora_wifi"] } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 88f86034aeaae..9a65af96852a1 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -31,11 +31,10 @@ "tag", "timer", "usb", - "updater", "webhook", "zeroconf", "zone" ], "codeowners": [], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 317ee21a9b04c..07fa93d976c48 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,6 +3,7 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle", "@Emilv2"], - "requirements": ["pydelijn==0.6.1"], - "iot_class": "cloud_polling" + "requirements": ["pydelijn==1.0.0"], + "iot_class": "cloud_polling", + "loggers": ["pydelijn"] } diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index dea3fcafcded3..e04385dcf3d0a 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -67,7 +67,6 @@ async def async_setup_platform( sensors.append( DeLijnPublicTransportSensor( Passages( - hass.loop, nextpassage[CONF_STOP_ID], nextpassage[CONF_NUMBER_OF_DEPARTURES], api_key, diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 8539a69e560d4..5bf4651096c63 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": ["deluge-client==1.7.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["deluge_client"] } diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 3ad8482509031..abee8310e171e 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -3,7 +3,7 @@ import datetime from random import random -from homeassistant import bootstrap, config_entries +from homeassistant import config_entries, setup from homeassistant.components import persistent_notification from homeassistant.components.recorder.statistics import ( async_add_external_statistics, @@ -83,11 +83,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not hass.config.longitude: hass.config.longitude = 117.22743 - tasks = [bootstrap.async_setup_component(hass, "sun", config)] + tasks = [setup.async_setup_component(hass, "sun", config)] # Set up input select tasks.append( - bootstrap.async_setup_component( + setup.async_setup_component( hass, "input_select", { @@ -108,7 +108,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Set up input boolean tasks.append( - bootstrap.async_setup_component( + setup.async_setup_component( hass, "input_boolean", { @@ -125,7 +125,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Set up input button tasks.append( - bootstrap.async_setup_component( + setup.async_setup_component( hass, "input_button", { @@ -141,7 +141,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Set up input number tasks.append( - bootstrap.async_setup_component( + setup.async_setup_component( hass, "input_number", { @@ -280,7 +280,7 @@ async def finish_setup(hass, config): lights = sorted(hass.states.async_entity_ids("light")) # Set up scripts - await bootstrap.async_setup_component( + await setup.async_setup_component( hass, "script", { @@ -309,7 +309,7 @@ async def finish_setup(hass, config): ) # Set up scenes - await bootstrap.async_setup_component( + await setup.async_setup_component( hass, "scene", { diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 110753ac15f3d..b73e5444b22e4 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -33,7 +33,7 @@ async def async_setup_platform( [ ManualAlarm( hass, - "Alarm", + "Security", "1234", None, True, diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 89256c954686d..e70f5efc6264c 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -114,11 +114,11 @@ def __init__( self.hass = hass self._unique_id = unique_id self._supported_features = supported_features - self._percentage = None + self._percentage: int | None = None self._preset_modes = preset_modes - self._preset_mode = None - self._oscillating = None - self._direction = None + self._preset_mode: str | None = None + self._oscillating: bool | None = None + self._direction: str | None = None self._name = name if supported_features & SUPPORT_OSCILLATE: self._oscillating = False @@ -141,12 +141,12 @@ def should_poll(self): return False @property - def current_direction(self) -> str: + def current_direction(self) -> str | None: """Fan direction.""" return self._direction @property - def oscillating(self) -> bool: + def oscillating(self) -> bool | None: """Oscillating.""" return self._oscillating @@ -257,7 +257,7 @@ def preset_modes(self) -> list[str] | None: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in self.preset_modes: + if self.preset_modes is None or preset_mode not in self.preset_modes: raise ValueError( "{preset_mode} is not a valid preset_mode: {self.preset_modes}" ) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 13e15eb274ee0..9bb3a30686d55 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -195,17 +195,17 @@ def color_mode(self) -> str | None: return self._color_mode @property - def hs_color(self) -> tuple: + def hs_color(self) -> tuple[float, float]: """Return the hs color value.""" return self._hs_color @property - def rgbw_color(self) -> tuple: + def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value.""" return self._rgbw_color @property - def rgbww_color(self) -> tuple: + def rgbww_color(self) -> tuple[int, int, int, int, int]: """Return the rgbww color value.""" return self._rgbww_color diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 317babfe74c7c..661e218a1adc3 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,7 +1,10 @@ """Demo implementation of the media player.""" from __future__ import annotations -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -68,8 +71,8 @@ async def async_setup_entry( await async_setup_platform(hass, {}, async_add_entities) -SOUND_MODE_LIST = ["Dummy Music", "Dummy Movie"] -DEFAULT_SOUND_MODE = "Dummy Music" +SOUND_MODE_LIST = ["Music", "Movie"] +DEFAULT_SOUND_MODE = "Music" YOUTUBE_PLAYER_SUPPORT = ( SUPPORT_PAUSE @@ -449,6 +452,8 @@ class DemoTVShowPlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__(self): """Initialize the demo device.""" super().__init__("Lounge room") diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index dd22c5f982713..8660604af9ea7 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -105,13 +105,10 @@ def __init__( if step is not None: self._attr_step = step - @property - def device_info(self) -> DeviceInfo: - """Return device info.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={ # Serial numbers are unique identifiers within a specific domain - (DOMAIN, self.unique_id) + (DOMAIN, unique_id) }, name=self.name, ) diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 2e06b009c9cfd..f2c1ce11b0a69 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -46,7 +46,7 @@ def __init__(self, name: str | None, state: bool, icon: str | None) -> None: self._attr_name = name or DEVICE_DEFAULT_NAME self._attr_is_on = state self._attr_icon = icon - self._last_command_sent = None + self._last_command_sent: str | None = None @property def extra_state_attributes(self) -> dict[str, Any] | None: diff --git a/homeassistant/components/demo/siren.py b/homeassistant/components/demo/siren.py index 32d3de4b497ce..9cf18a4c9025d 100644 --- a/homeassistant/components/demo/siren.py +++ b/homeassistant/components/demo/siren.py @@ -54,7 +54,7 @@ class DemoSiren(SirenEntity): def __init__( self, name: str, - available_tones: str | None = None, + available_tones: list[str | int] | None = None, support_volume_set: bool = False, support_duration: bool = False, is_on: bool = True, diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index c61e39d085b98..217119e93721c 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -64,12 +64,8 @@ def __init__( self._attr_is_on = state self._attr_name = name or DEVICE_DEFAULT_NAME self._attr_unique_id = unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device info.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, name=self.name, ) diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index d617e4a6abedf..34afbe9df01a5 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc boolean", + "constant": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae", "int": "\u0391\u03c1\u03b9\u03b8\u03bc\u03b7\u03c4\u03b9\u03ba\u03ae \u03b5\u03af\u03c3\u03bf\u03b4\u03bf\u03c2" } }, @@ -15,5 +16,6 @@ } } } - } + }, + "title": "Demo" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index d987ee472e256..e543a83bfe5ca 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -3,7 +3,7 @@ "step": { "options_1": { "data": { - "bool": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u771f\u507d\u5024(booleans)", + "bool": "\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u771f\u507d\u5024(Booleans)", "constant": "\u5b9a\u6570", "int": "\u6570\u5024\u5165\u529b" } diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 8364f0bc94be2..49290be4ceb7e 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "Booleano opcional", + "constant": "Constante", "int": "Entrada num\u00e9rica" } }, diff --git a/homeassistant/components/demo/translations/select.el.json b/homeassistant/components/demo/translations/select.el.json new file mode 100644 index 0000000000000..e6c6e3e686ea8 --- /dev/null +++ b/homeassistant/components/demo/translations/select.el.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03c6\u03c9\u03c4\u03cc\u03c2", + "ludicrous_speed": "\u039b\u03c5\u03c3\u03c3\u03b1\u03bb\u03ad\u03b1 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1", + "ridiculous_speed": "\u0393\u03b5\u03bb\u03bf\u03af\u03b1 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.pt-BR.json b/homeassistant/components/demo/translations/select.pt-BR.json new file mode 100644 index 0000000000000..2530ff3b4ca53 --- /dev/null +++ b/homeassistant/components/demo/translations/select.pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocidade da luz", + "ludicrous_speed": "Velocidade absurda", + "ridiculous_speed": "Velocidade rid\u00edcula" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index a6678d8b53344..27594703fd23a 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -88,6 +88,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index c8d0e8a4d9d43..5675e573bc1ef 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -55,5 +55,6 @@ "deviceType": "urn:schemas-denon-com:device:AiosDevice:1" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["denonavr"] } diff --git a/homeassistant/components/denonavr/translations/el.json b/homeassistant/components/denonavr/translations/el.json index b648ed0573a88..180cacc245955 100644 --- a/homeassistant/components/denonavr/translations/el.json +++ b/homeassistant/components/denonavr/translations/el.json @@ -1,7 +1,32 @@ { "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", + "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac, \u03b7 \u03b1\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c4\u03c9\u03bd \u03ba\u03b1\u03bb\u03c9\u03b4\u03af\u03c9\u03bd \u03c1\u03b5\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 ethernet \u03ba\u03b1\u03b9 \u03b7 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03bf\u03b7\u03b8\u03ae\u03c3\u03b5\u03b9", + "not_denonavr_manufacturer": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR, \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b5 \u03cc\u03c4\u03b9 \u03bf \u03ba\u03b1\u03c4\u03b1\u03c3\u03ba\u03b5\u03c5\u03b1\u03c3\u03c4\u03ae\u03c2 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9", + "not_denonavr_missing": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR, \u03bf\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ae\u03c1\u03b5\u03b9\u03c2" + }, + "error": { + "discovery_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03b4\u03ad\u03ba\u03c4\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" + }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03ad\u03ba\u03c4\u03b7", + "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" + }, + "select": { + "data": { + "select_host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b4\u03ad\u03ba\u03c4\u03b7" + }, + "description": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03ad\u03ba\u03c4\u03b5\u03c2.", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b4\u03ad\u03ba\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5" + }, "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03ad\u03ba\u03c4\u03b7 \u03c3\u03b1\u03c2, \u03b5\u03ac\u03bd \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" } @@ -16,6 +41,7 @@ "zone2": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b6\u03ce\u03bd\u03b7\u03c2 2", "zone3": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b6\u03ce\u03bd\u03b7\u03c2 3" }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" } } diff --git a/homeassistant/components/denonavr/translations/es-419.json b/homeassistant/components/denonavr/translations/es-419.json index c506f9f6aac7d..e22b8feebd131 100644 --- a/homeassistant/components/denonavr/translations/es-419.json +++ b/homeassistant/components/denonavr/translations/es-419.json @@ -1,8 +1,37 @@ { + "config": { + "abort": { + "cannot_connect": "No se pudo conectar, intente nuevamente, desconectar la alimentaci\u00f3n el\u00e9ctrica y los cables de ethernet y volver a conectarlos puede ayudar", + "not_denonavr_manufacturer": "No es un receptor de red Denon AVR, el fabricante descubierto no coincide", + "not_denonavr_missing": "No es un receptor de red Denon AVR, la informaci\u00f3n de descubrimiento no est\u00e1 completa" + }, + "error": { + "discovery_error": "Error al descubrir un receptor de red Denon AVR" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Por favor, confirme la adici\u00f3n del receptor", + "title": "Receptores de red Denon AVR" + }, + "select": { + "data": { + "select_host": "Direcci\u00f3n IP del receptor" + }, + "description": "Vuelva a ejecutar la configuraci\u00f3n si desea conectar receptores adicionales", + "title": "Seleccione el receptor que desea conectar" + }, + "user": { + "description": "Con\u00e9ctese a su receptor, si la direcci\u00f3n IP no est\u00e1 configurada, se usa el descubrimiento autom\u00e1tico", + "title": "Receptores de red Denon AVR" + } + } + }, "options": { "step": { "init": { "data": { + "show_all_sources": "Mostrar todas las fuentes", "update_audyssey": "Actualizar la configuraci\u00f3n de Audyssey", "zone2": "Configurar Zona 2", "zone3": "Configurar Zona 3" diff --git a/homeassistant/components/denonavr/translations/pt-BR.json b/homeassistant/components/denonavr/translations/pt-BR.json new file mode 100644 index 0000000000000..084c7dd3c182d --- /dev/null +++ b/homeassistant/components/denonavr/translations/pt-BR.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar, tente novamente, desconectar os cabos de alimenta\u00e7\u00e3o e ethernet e reconect\u00e1-los pode ajudar", + "not_denonavr_manufacturer": "N\u00e3o \u00e9 um receptor de rede Denon AVR, o fabricante descoberto n\u00e3o corresponde", + "not_denonavr_missing": "N\u00e3o \u00e9 um receptor de rede Denon AVR, as informa\u00e7\u00f5es de descoberta n\u00e3o est\u00e3o completas" + }, + "error": { + "discovery_error": "Falha ao descobrir um receptor de rede Denon AVR" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Confirme a adi\u00e7\u00e3o do receptor", + "title": "Receptores de rede Denon AVR" + }, + "select": { + "data": { + "select_host": "Endere\u00e7o IP do receptor" + }, + "description": "Execute a configura\u00e7\u00e3o novamente se desejar conectar receptores adicionais", + "title": "Selecione o receptor que voc\u00ea deseja conectar" + }, + "user": { + "data": { + "host": "Endere\u00e7o IP" + }, + "description": "Conecte-se ao seu receptor, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada", + "title": "Receptores de rede Denon AVR" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Mostrar todas as fontes", + "update_audyssey": "Atualizar as configura\u00e7\u00f5es do Audyssey", + "zone2": "Configure a Zona 2", + "zone3": "Configurar a Zona 3" + }, + "description": "Especificar configura\u00e7\u00f5es opcionais", + "title": "Receptores de rede Denon AVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/sk.json b/homeassistant/components/denonavr/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/denonavr/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 65af74981cfb7..9be01f27e4d0f 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -1,6 +1,7 @@ """Numeric derivative of data coming from a source sensor over time.""" from __future__ import annotations +from datetime import timedelta from decimal import Decimal, DecimalException import logging @@ -112,7 +113,9 @@ def __init__( self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 - self._state_list = [] # List of tuples with (timestamp, sensor_value) + self._state_list = ( + [] + ) # List of tuples with (timestamp_start, timestamp_end, derivative) self._name = name if name is not None else f"{source_entity} derivative" @@ -149,39 +152,32 @@ def calc_derivative(event): ): return - now = new_state.last_updated - # Filter out the tuples that are older than (and outside of the) `time_window` - self._state_list = [ - (timestamp, state) - for timestamp, state in self._state_list - if (now - timestamp).total_seconds() < self._time_window - ] - # It can happen that the list is now empty, in that case - # we use the old_state, because we cannot do anything better. - if len(self._state_list) == 0: - self._state_list.append((old_state.last_updated, old_state.state)) - self._state_list.append((new_state.last_updated, new_state.state)) - if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) - try: - # derivative of previous measures. - last_time, last_value = self._state_list[-1] - first_time, first_value = self._state_list[0] + # filter out all derivatives older than `time_window` from our window list + self._state_list = [ + (time_start, time_end, state) + for time_start, time_end, state in self._state_list + if (new_state.last_updated - time_end).total_seconds() + < self._time_window + ] - elapsed_time = (last_time - first_time).total_seconds() - delta_value = Decimal(last_value) - Decimal(first_value) - derivative = ( + try: + elapsed_time = ( + new_state.last_updated - old_state.last_updated + ).total_seconds() + delta_value = Decimal(new_state.state) - Decimal(old_state.state) + new_derivative = ( delta_value / Decimal(elapsed_time) / Decimal(self._unit_prefix) * Decimal(self._unit_time) ) - assert isinstance(derivative, Decimal) + except ValueError as err: _LOGGER.warning("While calculating derivative: %s", err) except DecimalException as err: @@ -190,9 +186,32 @@ def calc_derivative(event): ) except AssertionError as err: _LOGGER.error("Could not calculate derivative: %s", err) + + # add latest derivative to the window list + self._state_list.append( + (old_state.last_updated, new_state.last_updated, new_derivative) + ) + + def calculate_weight(start, end, now): + window_start = now - timedelta(seconds=self._time_window) + if start < window_start: + weight = (end - window_start).total_seconds() / self._time_window + else: + weight = (end - start).total_seconds() / self._time_window + return weight + + # If outside of time window just report derivative (is the same as modeling it in the window), + # otherwise take the weighted average with the previous derivatives + if elapsed_time > self._time_window: + derivative = new_derivative else: - self._state = derivative - self.async_write_ha_state() + derivative = 0 + for (start, end, value) in self._state_list: + weight = calculate_weight(start, end, new_state.last_updated) + derivative = derivative + (value * Decimal(weight)) + + self._state = derivative + self.async_write_ha_state() async_track_state_change_event( self.hass, [self._sensor_source_id], calc_derivative diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index c8cbc5ba11e4c..1eeb2241db52d 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["schiene"] } diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 603e88fd8c8e0..75613b3d118fd 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -7,7 +7,7 @@ from functools import wraps import logging from types import ModuleType -from typing import Any, NamedTuple +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Union, overload import voluptuous as vol import voluptuous_serialize @@ -27,6 +27,18 @@ from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig +if TYPE_CHECKING: + from .action import DeviceAutomationActionProtocol + from .condition import DeviceAutomationConditionProtocol + from .trigger import DeviceAutomationTriggerProtocol + + DeviceAutomationPlatformType = Union[ + ModuleType, + DeviceAutomationTriggerProtocol, + DeviceAutomationConditionProtocol, + DeviceAutomationActionProtocol, + ] + # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" @@ -115,9 +127,43 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +@overload +async def async_get_device_automation_platform( # noqa: D103 + hass: HomeAssistant, + domain: str, + automation_type: Literal[DeviceAutomationType.TRIGGER], +) -> DeviceAutomationTriggerProtocol: + ... + + +@overload +async def async_get_device_automation_platform( # noqa: D103 + hass: HomeAssistant, + domain: str, + automation_type: Literal[DeviceAutomationType.CONDITION], +) -> DeviceAutomationConditionProtocol: + ... + + +@overload +async def async_get_device_automation_platform( # noqa: D103 + hass: HomeAssistant, + domain: str, + automation_type: Literal[DeviceAutomationType.ACTION], +) -> DeviceAutomationActionProtocol: + ... + + +@overload +async def async_get_device_automation_platform( # noqa: D103 + hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str +) -> "DeviceAutomationPlatformType": + ... + + async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str -) -> ModuleType: +) -> "DeviceAutomationPlatformType": """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py new file mode 100644 index 0000000000000..5261757c645cf --- /dev/null +++ b/homeassistant/components/device_automation/action.py @@ -0,0 +1,68 @@ +"""Device action validator.""" +from __future__ import annotations + +from typing import Any, Protocol, cast + +import voluptuous as vol + +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import DeviceAutomationType, async_get_device_automation_platform +from .exceptions import InvalidDeviceAutomationConfig + + +class DeviceAutomationActionProtocol(Protocol): + """Define the format of device_action modules. + + Each module must define either ACTION_SCHEMA or async_validate_action_config. + """ + + ACTION_SCHEMA: vol.Schema + + async def async_validate_action_config( + self, hass: HomeAssistant, config: ConfigType + ) -> ConfigType: + """Validate config.""" + raise NotImplementedError + + async def async_call_action_from_config( + self, + hass: HomeAssistant, + config: ConfigType, + variables: dict[str, Any], + context: Context | None, + ) -> None: + """Execute a device action.""" + raise NotImplementedError + + +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + try: + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], DeviceAutomationType.ACTION + ) + if hasattr(platform, "async_validate_action_config"): + return await platform.async_validate_action_config(hass, config) + return cast(ConfigType, platform.ACTION_SCHEMA(config)) + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid action configuration") from err + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: dict[str, Any], + context: Context | None, +) -> None: + """Execute a device action.""" + platform = await async_get_device_automation_platform( + hass, + config[CONF_DOMAIN], + DeviceAutomationType.ACTION, + ) + await platform.async_call_action_from_config(hass, config, variables, context) diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py new file mode 100644 index 0000000000000..1c226ee8c2929 --- /dev/null +++ b/homeassistant/components/device_automation/condition.py @@ -0,0 +1,64 @@ +"""Validate device conditions.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol, cast + +import voluptuous as vol + +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from . import DeviceAutomationType, async_get_device_automation_platform +from .exceptions import InvalidDeviceAutomationConfig + +if TYPE_CHECKING: + from homeassistant.helpers import condition + + +class DeviceAutomationConditionProtocol(Protocol): + """Define the format of device_condition modules. + + Each module must define either CONDITION_SCHEMA or async_validate_condition_config. + """ + + CONDITION_SCHEMA: vol.Schema + + async def async_validate_condition_config( + self, hass: HomeAssistant, config: ConfigType + ) -> ConfigType: + """Validate config.""" + raise NotImplementedError + + def async_condition_from_config( + self, hass: HomeAssistant, config: ConfigType + ) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + raise NotImplementedError + + +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate device condition config.""" + try: + config = cv.DEVICE_CONDITION_SCHEMA(config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION + ) + if hasattr(platform, "async_validate_condition_config"): + return await platform.async_validate_condition_config(hass, config) + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) + except InvalidDeviceAutomationConfig as err: + raise vol.Invalid(str(err) or "Invalid condition configuration") from err + + +async def async_condition_from_config( + hass: HomeAssistant, config: ConfigType +) -> condition.ConditionCheckerType: + """Test a device condition.""" + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION + ) + return platform.async_condition_from_config(hass, config) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index 008a7603dba60..933c5c4c60a75 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,7 +1,15 @@ """Offer device oriented automation.""" +from typing import Protocol, cast + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import ( DEVICE_TRIGGER_BASE_SCHEMA, @@ -10,26 +18,55 @@ ) from .exceptions import InvalidDeviceAutomationConfig -# mypy: allow-untyped-defs, no-check-untyped-defs - TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -async def async_validate_trigger_config(hass, config): - """Validate config.""" - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER - ) - if not hasattr(platform, "async_validate_trigger_config"): - return platform.TRIGGER_SCHEMA(config) +class DeviceAutomationTriggerProtocol(Protocol): + """Define the format of device_trigger modules. + Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config. + """ + + TRIGGER_SCHEMA: vol.Schema + + async def async_validate_trigger_config( + self, hass: HomeAssistant, config: ConfigType + ) -> ConfigType: + """Validate config.""" + raise NotImplementedError + + async def async_attach_trigger( + self, + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, + ) -> CALLBACK_TYPE: + """Attach a trigger.""" + raise NotImplementedError + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" try: - return await getattr(platform, "async_validate_trigger_config")(hass, config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER + ) + if not hasattr(platform, "async_validate_trigger_config"): + return cast(ConfigType, platform.TRIGGER_SCHEMA(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 -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for trigger.""" platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 18d769df07f09..adabd297c551e 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -16,12 +16,21 @@ ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.typing import StateType -from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, LOGGER +from .const import ( + ATTR_HOST_NAME, + ATTR_IP, + ATTR_MAC, + ATTR_SOURCE_TYPE, + CONNECTED_DEVICE_REGISTERED, + DOMAIN, + LOGGER, +) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -64,9 +73,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@callback +def _async_connected_device_registered( + hass: HomeAssistant, mac: str, ip_address: str | None, hostname: str | None +) -> None: + """Register a newly seen connected device. + + This is currently used by the dhcp integration + to listen for newly registered connected devices + for discovery. + """ + async_dispatcher_send( + hass, + CONNECTED_DEVICE_REGISTERED, + { + ATTR_IP: ip_address, + ATTR_MAC: mac, + ATTR_HOST_NAME: hostname, + }, + ) + + @callback def _async_register_mac( - hass: HomeAssistant, domain: str, mac: str, unique_id: str + hass: HomeAssistant, + domain: str, + mac: str, + unique_id: str, ) -> None: """Register a mac address with a unique ID.""" data_key = "device_tracker_mac" @@ -108,14 +141,11 @@ def handle_device_event(ev: Event) -> None: return ent_reg = er.async_get(hass) - entity_id = ent_reg.async_get_entity_id(DOMAIN, *unique_id) - if entity_id is None: + if (entity_id := ent_reg.async_get_entity_id(DOMAIN, *unique_id)) is None: return - entity_entry = ent_reg.async_get(entity_id) - - if entity_entry is None: + if (entity_entry := ent_reg.async_get(entity_id)) is None: return # Make sure entity has a config entry and was disabled by the @@ -297,8 +327,18 @@ def add_to_platform_start( super().add_to_platform_start(hass, platform, parallel_updates) if self.mac_address and self.unique_id: _async_register_mac( - hass, platform.platform_name, self.mac_address, self.unique_id + hass, + platform.platform_name, + self.mac_address, + self.unique_id, ) + if self.is_connected: + _async_connected_device_registered( + hass, + self.mac_address, + self.ip_address, + self.hostname, + ) @callback def find_device_entry(self) -> dr.DeviceEntry | None: diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 216255b9cb6cb..c52241ae51f39 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -37,3 +37,5 @@ ATTR_SOURCE_TYPE: Final = "source_type" ATTR_CONSIDER_HOME: Final = "consider_home" ATTR_IP: Final = "ip" + +CONNECTED_DEVICE_REGISTERED: Final = "device_tracker_connected_device_registered" diff --git a/homeassistant/components/device_tracker/translations/el.json b/homeassistant/components/device_tracker/translations/el.json index e7acdc0562e8b..7d42d82534538 100644 --- a/homeassistant/components/device_tracker/translations/el.json +++ b/homeassistant/components/device_tracker/translations/el.json @@ -1,5 +1,9 @@ { "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03c3\u03c0\u03af\u03c4\u03b9", + "is_not_home": "{entity_name} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c0\u03af\u03c4\u03b9" + }, "trigger_type": { "enters": "{entity_name} \u03b5\u03b9\u03c3\u03ad\u03c1\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03b6\u03ce\u03bd\u03b7", "leaves": "{entity_name} \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b6\u03ce\u03bd\u03b7" diff --git a/homeassistant/components/device_tracker/translations/pt-BR.json b/homeassistant/components/device_tracker/translations/pt-BR.json index c20638a4a6131..762fb96fd05a1 100644 --- a/homeassistant/components/device_tracker/translations/pt-BR.json +++ b/homeassistant/components/device_tracker/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} est\u00e1 em casa", + "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" + }, + "trigger_type": { + "enters": "{entity_name} entra em uma zona", + "leaves": "{entity_name} sai de uma zona" + } + }, "state": { "_": { "home": "Em casa", diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 9621a49157a90..e9076e3d3daa6 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -8,5 +8,6 @@ "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "silver", "iot_class": "local_push", - "zeroconf": ["_dvl-deviceapi._tcp.local."] + "zeroconf": ["_dvl-deviceapi._tcp.local."], + "loggers": ["devolo_home_control_api"] } diff --git a/homeassistant/components/devolo_home_control/translations/el.json b/homeassistant/components/devolo_home_control/translations/el.json new file mode 100644 index 0000000000000..b79dfee951299 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/el.json @@ -0,0 +1,28 @@ +{ + "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", + "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_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_failed": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 mydevolo \u03cc\u03c0\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03b9\u03bd." + }, + "step": { + "user": { + "data": { + "mydevolo_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 mydevolo", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email / devolo ID" + } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email / \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc devolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/pt-BR.json b/homeassistant/components/devolo_home_control/translations/pt-BR.json index 4a9930dd95d35..c2136958ffbc7 100644 --- a/homeassistant/components/devolo_home_control/translations/pt-BR.json +++ b/homeassistant/components/devolo_home_control/translations/pt-BR.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_failed": "Por favor, use o mesmo usu\u00e1rio mydevolo de antes." + }, "step": { "user": { "data": { - "password": "Senha" + "mydevolo_url": "mydevolo URL", + "password": "Senha", + "username": "Email / devolo ID" + } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Senha", + "username": "Email / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_control/translations/sk.json b/homeassistant/components/devolo_home_control/translations/sk.json new file mode 100644 index 0000000000000..9273954369f8d --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email / devolo ID" + } + }, + "zeroconf_confirm": { + "data": { + "username": "Email / devolo ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 0c6aeabf648a9..c96126f43e2af 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE @@ -48,7 +47,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors: dict = {} @@ -92,7 +93,7 @@ async def async_step_zeroconf( return await self.async_step_zeroconf_confirm() async def async_step_zeroconf_confirm( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by zeroconf.""" title = self.context["title_placeholders"][CONF_NAME] diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 85f4e1caf9b75..a514606a32225 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -7,5 +7,6 @@ "zeroconf": ["_dvl-deviceapi._tcp.local."], "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["devolo_plc_api"] } diff --git a/homeassistant/components/devolo_home_network/translations/cs.json b/homeassistant/components/devolo_home_network/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/el.json b/homeassistant/components/devolo_home_network/translations/el.json index 78e103f8b6902..45a54b59b00dd 100644 --- a/homeassistant/components/devolo_home_network/translations/el.json +++ b/homeassistant/components/devolo_home_network/translations/el.json @@ -1,6 +1,21 @@ { "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." + }, + "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" + }, + "flow_title": "{product} ({name})", "step": { + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "description": "\u03c8" + }, "zeroconf_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 devolo \u03bc\u03b5 \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 `{host_name}` \u03c3\u03c4\u03bf Home Assistant;", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 devolo" diff --git a/homeassistant/components/devolo_home_network/translations/pt-BR.json b/homeassistant/components/devolo_home_network/translations/pt-BR.json index edffd23f3afb1..94a1f632d788e 100644 --- a/homeassistant/components/devolo_home_network/translations/pt-BR.json +++ b/homeassistant/components/devolo_home_network/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "home_control": "A Unidade Central de Home Control Devolo n\u00e3o funciona com esta integra\u00e7\u00e3o." }, "error": { diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 8db69b389279c..7b5ae85bdff6f 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -81,6 +81,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def update_listener(hass, entry): +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/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 6133a67bcf16e..25193019f7ddb 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,8 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.2"], + "requirements": ["pydexcom==0.2.3"], "codeowners": ["@gagebenne"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pydexcom"] } diff --git a/homeassistant/components/dexcom/translations/el.json b/homeassistant/components/dexcom/translations/el.json index b30be708065e4..29ca3b114caf4 100644 --- a/homeassistant/components/dexcom/translations/el.json +++ b/homeassistant/components/dexcom/translations/el.json @@ -1,8 +1,32 @@ { "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" + }, "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "server": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 Dexcom Share", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/pt-BR.json b/homeassistant/components/dexcom/translations/pt-BR.json new file mode 100644 index 0000000000000..ce21e4d51c8d9 --- /dev/null +++ b/homeassistant/components/dexcom/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "server": "Servidor", + "username": "Usu\u00e1rio" + }, + "description": "Insira as credenciais do Dexcom Share", + "title": "Configurar integra\u00e7\u00e3o Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de medida" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/sk.json b/homeassistant/components/dexcom/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/dexcom/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index dd247c4cab9e2..0b5f8a49a3476 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,5 +1,7 @@ """The dhcp integration.""" +from __future__ import annotations +from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta import fnmatch @@ -24,6 +26,7 @@ ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, + CONNECTED_DEVICE_REGISTERED, DOMAIN as DEVICE_TRACKER_DOMAIN, SOURCE_TYPE_ROUTER, ) @@ -35,7 +38,13 @@ from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceRegistry, + async_get, + format_mac, +) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, @@ -54,9 +63,11 @@ HOSTNAME: Final = "hostname" MAC_ADDRESS: Final = "macaddress" IP_ADDRESS: Final = "ip" +REGISTERED_DEVICES: Final = "registered_devices" DHCP_REQUEST = 3 SCAN_INTERVAL = timedelta(minutes=60) + _LOGGER = logging.getLogger(__name__) @@ -101,16 +112,23 @@ def get(self, name: str, default: Any = None) -> Any: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" + watchers: list[WatcherBase] = [] + address_data: dict[str, dict[str, str]] = {} + integration_matchers = await async_get_dhcp(hass) + + # For the passive classes we need to start listening + # for state changes and connect the dispatchers before + # everything else starts up or we will miss events + for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher): + passive_watcher = passive_cls(hass, address_data, integration_matchers) + await passive_watcher.async_start() + watchers.append(passive_watcher) async def _initialize(_): - address_data = {} - integration_matchers = await async_get_dhcp(hass) - watchers = [] - - for cls in (DHCPWatcher, DeviceTrackerWatcher, NetworkWatcher): - watcher = cls(hass, address_data, integration_matchers) - await watcher.async_start() - watchers.append(watcher) + for active_cls in (DHCPWatcher, NetworkWatcher): + active_watcher = active_cls(hass, address_data, integration_matchers) + await active_watcher.async_start() + watchers.append(active_watcher) async def _async_stop(*_): for watcher in watchers: @@ -133,7 +151,15 @@ def __init__(self, hass, address_data, integration_matchers): self._integration_matchers = integration_matchers self._address_data = address_data - def process_client(self, ip_address, hostname, mac_address): + @abstractmethod + async def async_stop(self): + """Stop the watcher.""" + + @abstractmethod + async def async_start(self): + """Start the watcher.""" + + def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: """Process a client.""" return run_callback_threadsafe( self.hass.loop, @@ -144,7 +170,9 @@ def process_client(self, ip_address, hostname, mac_address): ).result() @callback - def async_process_client(self, ip_address, hostname, mac_address): + def async_process_client( + self, ip_address: str, hostname: str, mac_address: str + ) -> None: """Process a client.""" made_ip_address = make_ip_address(ip_address) @@ -180,7 +208,20 @@ def async_process_client(self, ip_address, hostname, mac_address): ) matched_domains = set() + device_domains = set() + + dev_reg: DeviceRegistry = async_get(self.hass) + if device := dev_reg.async_get_device( + identifiers=set(), connections={(CONNECTION_NETWORK_MAC, uppercase_mac)} + ): + for entry_id in device.config_entries: + if entry := self.hass.config_entries.async_get_entry(entry_id): + device_domains.add(entry.domain) + for entry in self._integration_matchers: + if entry.get(REGISTERED_DEVICES) and not entry["domain"] in device_domains: + continue + if MAC_ADDRESS in entry and not fnmatch.fnmatch( uppercase_mac, entry[MAC_ADDRESS] ): @@ -192,14 +233,12 @@ def async_process_client(self, ip_address, hostname, mac_address): continue _LOGGER.debug("Matched %s against %s", data, entry) - if entry["domain"] in matched_domains: - # Only match once per domain - continue - matched_domains.add(entry["domain"]) + + for domain in matched_domains: discovery_flow.async_create_flow( self.hass, - entry["domain"], + domain, {"source": config_entries.SOURCE_DHCP}, DhcpServiceInfo( ip=ip_address, @@ -301,6 +340,39 @@ def _async_process_device_state(self, state: State): self.async_process_client(ip_address, hostname, _format_mac(mac_address)) +class DeviceTrackerRegisteredWatcher(WatcherBase): + """Class to watch data from device tracker registrations.""" + + def __init__(self, hass, address_data, integration_matchers): + """Initialize class.""" + super().__init__(hass, address_data, integration_matchers) + self._unsub = None + + async def async_stop(self): + """Stop watching for device tracker registrations.""" + if self._unsub: + self._unsub() + self._unsub = None + + async def async_start(self): + """Stop watching for device tracker registrations.""" + self._unsub = async_dispatcher_connect( + self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data + ) + + @callback + def _async_process_device_data(self, data: dict[str, str | None]) -> None: + """Process a device tracker state.""" + ip_address = data[ATTR_IP] + hostname = data[ATTR_HOST_NAME] or "" + mac_address = data[ATTR_MAC] + + if ip_address is None or mac_address is None: + return + + self.async_process_client(ip_address, hostname, _format_mac(mac_address)) + + class DHCPWatcher(WatcherBase): """Class to watch dhcp requests.""" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index c79d63dda16a3..fb9ebc70408be 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,8 +2,9 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.7"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.8"], "codeowners": ["@bdraco"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"] } diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index f8a38971a95be..a3f1e5fe27204 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -4,7 +4,7 @@ from http import HTTPStatus import json import logging -from typing import Protocol +from typing import Any, Protocol from aiohttp import web import voluptuous as vol @@ -51,12 +51,12 @@ class DiagnosticsProtocol(Protocol): async def async_get_config_entry_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry - ) -> dict: + ) -> Any: """Return diagnostics for a config entry.""" async def async_get_device_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry - ) -> dict: + ) -> Any: """Return diagnostics for a device.""" @@ -106,9 +106,8 @@ def handle_get( ): """List all possible diagnostic handlers.""" domain = msg["domain"] - info = hass.data[DOMAIN].get(domain) - if info is None: + if (info := hass.data[DOMAIN].get(domain)) is None: connection.send_error( msg["id"], websocket_api.ERR_NOT_FOUND, "Domain not supported" ) @@ -125,7 +124,7 @@ def handle_get( async def _async_get_json_file_response( hass: HomeAssistant, - data: dict | list, + data: Any, filename: str, domain: str, d_type: DiagnosticsType, @@ -197,14 +196,11 @@ async def get( # pylint: disable=no-self-use return web.Response(status=HTTPStatus.BAD_REQUEST) hass = request.app["hass"] - config_entry = hass.config_entries.async_get_entry(d_id) - if config_entry is None: + if (config_entry := hass.config_entries.async_get_entry(d_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) - info = hass.data[DOMAIN].get(config_entry.domain) - - if info is None: + if (info := hass.data[DOMAIN].get(config_entry.domain)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) filename = f"{config_entry.domain}-{config_entry.entry_id}" @@ -226,9 +222,8 @@ async def get( # pylint: disable=no-self-use dev_reg = async_get(hass) assert sub_id - device = dev_reg.async_get(sub_id) - if device is None: + if (device := dev_reg.async_get(sub_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) filename += f"-{device.name}-{device.id}" diff --git a/homeassistant/components/diagnostics/translations/es.json b/homeassistant/components/diagnostics/translations/es.json new file mode 100644 index 0000000000000..2ae994c70c997 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Diagn\u00f3sticos" +} \ No newline at end of file diff --git a/homeassistant/components/diagnostics/translations/fr.json b/homeassistant/components/diagnostics/translations/fr.json index cfa7ba1e7553c..f5936aced5b62 100644 --- a/homeassistant/components/diagnostics/translations/fr.json +++ b/homeassistant/components/diagnostics/translations/fr.json @@ -1,3 +1,3 @@ { - "title": "Diagnostiques" + "title": "Diagnostics" } \ No newline at end of file diff --git a/homeassistant/components/diagnostics/translations/id.json b/homeassistant/components/diagnostics/translations/id.json new file mode 100644 index 0000000000000..732e52ee843f4 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Diagnostik" +} \ No newline at end of file diff --git a/homeassistant/components/diagnostics/translations/nl.json b/homeassistant/components/diagnostics/translations/nl.json new file mode 100644 index 0000000000000..b12cbdbb1db03 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Diagnostiek" +} \ No newline at end of file diff --git a/homeassistant/components/diagnostics/translations/zh-Hans.json b/homeassistant/components/diagnostics/translations/zh-Hans.json new file mode 100644 index 0000000000000..6baa072147010 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/zh-Hans.json @@ -0,0 +1,3 @@ +{ + "title": "\u8bca\u65ad" +} \ No newline at end of file diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index 6154dd14bd21d..ba4f3d20f9a0c 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping -from typing import Any, TypeVar, cast +from typing import Any, TypeVar, cast, overload from homeassistant.core import callback @@ -11,6 +11,16 @@ T = TypeVar("T") +@overload +def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: ignore[misc] + ... + + +@overload +def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: + ... + + @callback def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: """Redact sensitive data in a dict.""" @@ -25,7 +35,7 @@ def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: for key, value in redacted.items(): if key in to_redact: redacted[key] = REDACTED - elif isinstance(value, dict): + elif isinstance(value, Mapping): redacted[key] = async_redact_data(value, to_redact) elif isinstance(value, list): redacted[key] = [async_redact_data(item, to_redact) for item in value] diff --git a/homeassistant/components/dialogflow/translations/bg.json b/homeassistant/components/dialogflow/translations/bg.json index d27bddfcd05db..2de3c1a479b64 100644 --- a/homeassistant/components/dialogflow/translations/bg.json +++ b/homeassistant/components/dialogflow/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/dialogflow/translations/ca.json b/homeassistant/components/dialogflow/translations/ca.json index dad18be0f6a0f..c59076ec57dae 100644 --- a/homeassistant/components/dialogflow/translations/ca.json +++ b/homeassistant/components/dialogflow/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/dialogflow/translations/de.json b/homeassistant/components/dialogflow/translations/de.json index 2035b818b44d1..bf6a4aca12899 100644 --- a/homeassistant/components/dialogflow/translations/de.json +++ b/homeassistant/components/dialogflow/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/dialogflow/translations/el.json b/homeassistant/components/dialogflow/translations/el.json index aecb2ee553fa4..b2d36ed6235fa 100644 --- a/homeassistant/components/dialogflow/translations/el.json +++ b/homeassistant/components/dialogflow/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd [\u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 webhook \u03c4\u03bf\u03c5 Dialogflow]( {dialogflow_url} ). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/json \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \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." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03b2\u03ad\u03b2\u03b1\u03b9\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Dialogflow;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Dialogflow Webhook" + } } } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/en.json b/homeassistant/components/dialogflow/translations/en.json index 31b7f1f8880bb..9c8157ce06db3 100644 --- a/homeassistant/components/dialogflow/translations/en.json +++ b/homeassistant/components/dialogflow/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json index 8ffe23497efe8..09cee8a724a34 100644 --- a/homeassistant/components/dialogflow/translations/et.json +++ b/homeassistant/components/dialogflow/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/dialogflow/translations/fr.json b/homeassistant/components/dialogflow/translations/fr.json index 1eb34e210003c..302c0df7f0567 100644 --- a/homeassistant/components/dialogflow/translations/fr.json +++ b/homeassistant/components/dialogflow/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/dialogflow/translations/he.json b/homeassistant/components/dialogflow/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/dialogflow/translations/he.json +++ b/homeassistant/components/dialogflow/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 23a6001d77c1c..69fdaea4d0051 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/dialogflow/translations/id.json b/homeassistant/components/dialogflow/translations/id.json index 046a04b1dc49b..e6dd34cb64d60 100644 --- a/homeassistant/components/dialogflow/translations/id.json +++ b/homeassistant/components/dialogflow/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/dialogflow/translations/it.json b/homeassistant/components/dialogflow/translations/it.json index b7b04c7886390..5f8c44d358b17 100644 --- a/homeassistant/components/dialogflow/translations/it.json +++ b/homeassistant/components/dialogflow/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 0cb6f57eae2f3..199db0c326c1d 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/dialogflow/translations/nb.json b/homeassistant/components/dialogflow/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/nl.json b/homeassistant/components/dialogflow/translations/nl.json index 82fe7daea0015..3d2617d25e500 100644 --- a/homeassistant/components/dialogflow/translations/nl.json +++ b/homeassistant/components/dialogflow/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/dialogflow/translations/no.json b/homeassistant/components/dialogflow/translations/no.json index af11f5c1c6371..cceb7dd033d93 100644 --- a/homeassistant/components/dialogflow/translations/no.json +++ b/homeassistant/components/dialogflow/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/dialogflow/translations/pl.json b/homeassistant/components/dialogflow/translations/pl.json index c90ed20af7494..2ca14c224d9bf 100644 --- a/homeassistant/components/dialogflow/translations/pl.json +++ b/homeassistant/components/dialogflow/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/dialogflow/translations/pt-BR.json b/homeassistant/components/dialogflow/translations/pt-BR.json index 45aadbd173075..7b5c5a96464d1 100644 --- a/homeassistant/components/dialogflow/translations/pt-BR.json +++ b/homeassistant/components/dialogflow/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Integra\u00e7\u00e3o do webhook da Dialogflow] ( {dialogflow_url} ). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application / json \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." }, diff --git a/homeassistant/components/dialogflow/translations/ru.json b/homeassistant/components/dialogflow/translations/ru.json index 55768c4f56719..ee8bfebadf97d 100644 --- a/homeassistant/components/dialogflow/translations/ru.json +++ b/homeassistant/components/dialogflow/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json index 520424e434fe2..27378ab328422 100644 --- a/homeassistant/components/dialogflow/translations/tr.json +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/dialogflow/translations/uk.json b/homeassistant/components/dialogflow/translations/uk.json index 625d2db78dcb0..5186a1882f14b 100644 --- a/homeassistant/components/dialogflow/translations/uk.json +++ b/homeassistant/components/dialogflow/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/dialogflow/translations/zh-Hans.json b/homeassistant/components/dialogflow/translations/zh-Hans.json index ae414f99e5540..a67c1ab76e1c9 100644 --- a/homeassistant/components/dialogflow/translations/zh-Hans.json +++ b/homeassistant/components/dialogflow/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Dialogflow \u7684 Webhook \u96c6\u6210]({dialogflow_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, diff --git a/homeassistant/components/dialogflow/translations/zh-Hant.json b/homeassistant/components/dialogflow/translations/zh-Hant.json index 4584a3831363c..0b66702610052 100644 --- a/homeassistant/components/dialogflow/translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index eba3626a95085..93c962f2d6c5e 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": ["python-digitalocean==1.13.2"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["digitalocean"] } diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 35cc1413bdfaa..51d5982a595ec 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": ["dlipower==0.7.165"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["dlipower"] } diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 3fba13121f1b6..d6fc946ab79a6 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -12,5 +12,6 @@ "deviceType": "urn:schemas-upnp-org:device:MediaServer:1" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["directv"] } diff --git a/homeassistant/components/directv/translations/el.json b/homeassistant/components/directv/translations/el.json new file mode 100644 index 0000000000000..49c7f5aa0bc52 --- /dev/null +++ b/homeassistant/components/directv/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", + "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" + }, + "flow_title": "{name}", + "step": { + "ssdp_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/pt-BR.json b/homeassistant/components/directv/translations/pt-BR.json index 277606b855b03..98fa2d6e3b6b5 100644 --- a/homeassistant/components/directv/translations/pt-BR.json +++ b/homeassistant/components/directv/translations/pt-BR.json @@ -1,9 +1,21 @@ { "config": { - "flow_title": "DirecTV: {name}", + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Voc\u00ea quer configurar o {name}?" + }, + "user": { + "data": { + "host": "Nome do host" + } } } } diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 5cc2d900229f7..4073cb273d8f8 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": ["discogs_client==2.3.0"], "codeowners": ["@thibmaek"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["discogs_client"] } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 0da186e792451..02b31a3aa999a 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,7 +2,8 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.7.3"], + "requirements": ["nextcord==2.0.0a8"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["discord"] } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index e8b084a01ab3c..41137e1a32cfe 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -1,8 +1,10 @@ """Discord platform for notify component.""" +from __future__ import annotations + import logging import os.path -import discord +import nextcord import voluptuous as vol from homeassistant.components.notify import ( @@ -48,8 +50,8 @@ def file_exists(self, filename): async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" - discord.VoiceClient.warn_nacl = False - discord_bot = discord.Client() + nextcord.VoiceClient.warn_nacl = False + discord_bot = nextcord.Client() images = None embedding = None @@ -59,13 +61,13 @@ async def async_send_message(self, message, **kwargs): data = kwargs.get(ATTR_DATA) or {} - embed = None + embeds: list[nextcord.Embed] = [] if ATTR_EMBED in data: embedding = data[ATTR_EMBED] fields = embedding.get(ATTR_EMBED_FIELDS) or [] if embedding: - embed = discord.Embed(**embedding) + embed = nextcord.Embed(**embedding) for field in fields: embed.add_field(**field) if ATTR_EMBED_FOOTER in embedding: @@ -74,11 +76,12 @@ async def async_send_message(self, message, **kwargs): embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) if ATTR_EMBED_THUMBNAIL in embedding: embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) + embeds.append(embed) if ATTR_IMAGES in data: images = [] - for image in data.get(ATTR_IMAGES): + for image in data.get(ATTR_IMAGES, []): image_exists = await self.hass.async_add_executor_job( self.file_exists, image ) @@ -95,15 +98,15 @@ async def async_send_message(self, message, **kwargs): channelid = int(channelid) try: channel = await discord_bot.fetch_channel(channelid) - except discord.NotFound: + except nextcord.NotFound: try: channel = await discord_bot.fetch_user(channelid) - except discord.NotFound: + except nextcord.NotFound: _LOGGER.warning("Channel not found for ID: %s", channelid) continue # Must create new instances of File for each channel. - files = [discord.File(image) for image in images] if images else None - await channel.send(message, files=files, embed=embed) - except (discord.HTTPException, discord.NotFound) as error: + files = [nextcord.File(image) for image in images] if images else [] + await channel.send(message, files=files, embeds=embeds) + except (nextcord.HTTPException, nextcord.NotFound) as error: _LOGGER.warning("Communication error: %s", error) await discord_bot.close() diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 1b7d51c1716e5..3e7d31fcb1cf0 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -5,5 +5,6 @@ "requirements": ["netdisco==3.0.0"], "after_dependencies": ["zeroconf"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "loggers": ["netdisco"] } diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index 792486c7a875d..8a0eb4304033d 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": ["face_recognition==1.2.3"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["face_recognition"] } diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index b8ac5bce5fa81..3932df60631e9 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": ["face_recognition==1.2.3"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["face_recognition"] } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 48a36a908c3c9..9319eb8dd0f30 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyW215"] } diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index 20a978f9fda2e..a4118a0ce78f6 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -21,6 +21,11 @@ CONNECT_TIMEOUT: Final = 10 +PROTOCOL_HTTP: Final = "http-get" +PROTOCOL_RTSP: Final = "rtsp-rtp-udp" +PROTOCOL_ANY: Final = "*" +STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] + # Map UPnP class to media_player media_content_type MEDIA_TYPE_MAP: Mapping[str, str] = { "object": _mp_const.MEDIA_TYPE_URL, diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index dfe4e8c1b96a3..4001fc9dddce7 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,8 +3,9 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.23.4"], + "requirements": ["async-upnp-client==0.23.5"], "dependencies": ["ssdp"], + "after_dependencies": ["media_source"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", @@ -20,5 +21,6 @@ } ], "codeowners": ["@StevenLooman", "@chishm"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["async_upnp_client"] } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index fd89c5be2d0dc..265c6e9dde6d3 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -13,16 +13,22 @@ from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState from async_upnp_client.utils import async_get_local_ip +from didl_lite import didl_lite from typing_extensions import Concatenate, ParamSpec from homeassistant import config_entries -from homeassistant.components import ssdp -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components import media_source, ssdp +from homeassistant.components.media_player import ( + BrowseMedia, + MediaPlayerEntity, + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, REPEAT_MODE_ALL, REPEAT_MODE_OFF, REPEAT_MODE_ONE, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -61,6 +67,7 @@ MEDIA_UPNP_CLASS_MAP, REPEAT_PLAY_MODES, SHUFFLE_PLAY_MODES, + STREAMABLE_PROTOCOLS, ) from .data import EventListenAddr, get_domain_data @@ -512,7 +519,7 @@ def supported_features(self) -> int: if self._device.can_next: supported_features |= SUPPORT_NEXT_TRACK if self._device.has_play_media: - supported_features |= SUPPORT_PLAY_MEDIA + supported_features |= SUPPORT_PLAY_MEDIA | SUPPORT_BROWSE_MEDIA if self._device.can_seek_rel_time: supported_features |= SUPPORT_SEEK @@ -586,10 +593,30 @@ async def async_play_media( """Play a piece of media.""" _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) assert self._device is not None + + didl_metadata: str | None = None + title: str = "" + + # If media is media_source, resolve it to url and MIME type, and maybe metadata + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_type = sourced_media.mime_type + media_id = sourced_media.url + _LOGGER.debug("sourced_media is %s", sourced_media) + if sourced_metadata := getattr(sourced_media, "didl_metadata", None): + didl_metadata = didl_lite.to_xml_string(sourced_metadata).decode( + "utf-8" + ) + title = sourced_metadata.title + + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) + extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} metadata: dict[str, Any] = extra.get("metadata") or {} - title = extra.get("title") or metadata.get("title") or "Home Assistant" + if not title: + title = extra.get("title") or metadata.get("title") or "Home Assistant" if thumb := extra.get("thumb"): metadata["album_art_uri"] = thumb @@ -598,15 +625,16 @@ async def async_play_media( if hass_key in metadata: metadata[didl_key] = metadata.pop(hass_key) - # Create metadata specific to the given media type; different fields are - # available depending on what the upnp_class is. - upnp_class = MEDIA_UPNP_CLASS_MAP.get(media_type) - didl_metadata = await self._device.construct_play_media_metadata( - media_url=media_id, - media_title=title, - override_upnp_class=upnp_class, - meta_data=metadata, - ) + if not didl_metadata: + # Create metadata specific to the given media type; different fields are + # available depending on what the upnp_class is. + upnp_class = MEDIA_UPNP_CLASS_MAP.get(media_type) + didl_metadata = await self._device.construct_play_media_metadata( + media_url=media_id, + media_title=title, + override_upnp_class=upnp_class, + meta_data=metadata, + ) # Stop current playing media if self._device.can_stop: @@ -726,6 +754,54 @@ async def async_select_sound_mode(self, sound_mode: str) -> None: assert self._device is not None await self._device.async_select_preset(sound_mode) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper. + + Browses all available media_sources by default. Filters content_type + based on the DMR's sink_protocol_info. + """ + _LOGGER.debug( + "async_browse_media(%s, %s)", media_content_type, media_content_id + ) + + # media_content_type is ignored; it's the content_type of the current + # media_content_id, not the desired content_type of whomever is calling. + + content_filter = self._get_content_filter() + + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=content_filter + ) + + def _get_content_filter(self) -> Callable[[BrowseMedia], bool]: + """Return a function that filters media based on what the renderer can play.""" + if not self._device or not self._device.sink_protocol_info: + # Nothing is specified by the renderer, so show everything + _LOGGER.debug("Get content filter with no device or sink protocol info") + return lambda _: True + + _LOGGER.debug("Get content filter for %s", self._device.sink_protocol_info) + if self._device.sink_protocol_info[0] == "*": + # Renderer claims it can handle everything, so show everything + return lambda _: True + + # Convert list of things like "http-get:*:audio/mpeg:*" to just "audio/mpeg" + content_types: list[str] = [] + for protocol_info in self._device.sink_protocol_info: + protocol, _, content_format, _ = protocol_info.split(":", 3) + if protocol in STREAMABLE_PROTOCOLS: + content_types.append(content_format) + + def _content_type_filter(item: BrowseMedia) -> bool: + """Filter media items by their content_type.""" + return item.media_content_type in content_types + + return _content_type_filter + @property def media_title(self) -> str | None: """Title of current playing media.""" diff --git a/homeassistant/components/dlna_dmr/translations/el.json b/homeassistant/components/dlna_dmr/translations/el.json new file mode 100644 index 0000000000000..91a1d353bccf2 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/el.json @@ -0,0 +1,58 @@ +{ + "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", + "alternative_integration": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "could_not_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae DLNA", + "discovery_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03c3\u03c4\u03bf\u03b9\u03c7\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 DLNA", + "incomplete_config": "\u0391\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae", + "non_unique_id": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", + "not_dmr": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c8\u03b7\u03c6\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "could_not_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae DLNA", + "not_dmr": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c8\u03b7\u03c6\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" + }, + "flow_title": "{name}", + "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;" + }, + "import_turn_on": { + "description": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, + "manual": { + "data": { + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, + "description": "URL \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf XML \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 DLNA DMR" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 DLNA DMR" + } + } + }, + "options": { + "error": { + "invalid_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2 \u03b1\u03ba\u03c1\u03bf\u03b1\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2", + "listen_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd (\u03c4\u03c5\u03c7\u03b1\u03af\u03b1 \u03b1\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af)", + "poll_availability": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 DLNA Digital Media Renderer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/es-419.json b/homeassistant/components/dlna_dmr/translations/es-419.json new file mode 100644 index 0000000000000..3dff768512273 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "alternative_integration": "El dispositivo es mejor compatible con otra integraci\u00f3n", + "could_not_connect": "No se pudo conectar al dispositivo DLNA", + "discovery_error": "Error al descubrir un dispositivo DLNA coincidente", + "incomplete_config": "A la configuraci\u00f3n le falta una variable requerida", + "non_unique_id": "Varios dispositivos encontrados con la misma ID \u00fanica", + "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" + }, + "error": { + "could_not_connect": "No se pudo conectar al dispositivo DLNA", + "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" + }, + "flow_title": "{name}", + "step": { + "import_turn_on": { + "description": "Encienda el dispositivo y haga clic en Enviar para continuar con la migraci\u00f3n." + }, + "manual": { + "description": "URL a un archivo XML de descripci\u00f3n de dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pt-BR.json b/homeassistant/components/dlna_dmr/translations/pt-BR.json new file mode 100644 index 0000000000000..0df5a60e56528 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/pt-BR.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "alternative_integration": "O dispositivo \u00e9 melhor suportado por outra integra\u00e7\u00e3o", + "cannot_connect": "Falha ao conectar", + "could_not_connect": "Falha ao conectar ao dispositivo DLNA", + "discovery_error": "Falha ao descobrir um dispositivo DLNA correspondente", + "incomplete_config": "A configura\u00e7\u00e3o n\u00e3o tem uma vari\u00e1vel obrigat\u00f3ria", + "non_unique_id": "V\u00e1rios dispositivos encontrados com o mesmo ID exclusivo", + "not_dmr": "O dispositivo n\u00e3o \u00e9 um renderizador de m\u00eddia digital compat\u00edvel" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "could_not_connect": "Falha ao conectar-se ao dispositivo DLNA", + "not_dmr": "O dispositivo n\u00e3o \u00e9 um renderizador de m\u00eddia digital compat\u00edvel" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "import_turn_on": { + "description": "Por favor, ligue o dispositivo e clique em enviar para continuar a migra\u00e7\u00e3o" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL para um arquivo XML de descri\u00e7\u00e3o do dispositivo", + "title": "Conex\u00e3o manual do dispositivo DLNA DMR" + }, + "user": { + "data": { + "host": "Nome do host", + "url": "URL" + }, + "description": "Escolha um dispositivo para configurar ou deixe em branco para inserir um URL", + "title": "Dispositivos DMR DLNA descobertos" + } + } + }, + "options": { + "error": { + "invalid_url": "URL inv\u00e1lida" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL de retorno do ouvinte de eventos", + "listen_port": "Porta do ouvinte de eventos (aleat\u00f3rio se n\u00e3o estiver definido)", + "poll_availability": "Pesquisa de disponibilidade do dispositivo" + }, + "title": "Configura\u00e7\u00e3o do renderizador de m\u00eddia digital DLNA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/__init__.py b/homeassistant/components/dlna_dms/__init__.py new file mode 100644 index 0000000000000..b09547e07c814 --- /dev/null +++ b/homeassistant/components/dlna_dms/__init__.py @@ -0,0 +1,28 @@ +"""The DLNA Digital Media Server integration. + +A single config entry is used, with SSDP discovery for media servers. Each +server is wrapped in a DmsEntity, and the server's USN is used as the unique_id. +""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import LOGGER +from .dms import get_domain_data + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up DLNA DMS device from a config entry.""" + LOGGER.debug("Setting up config entry: %s", entry.unique_id) + + # Forward setup to this domain's data manager + return await get_domain_data(hass).async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + LOGGER.debug("Unloading config entry: %s", entry.unique_id) + + # Forward unload to this domain's data manager + return await get_domain_data(hass).async_unload_entry(entry) diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py new file mode 100644 index 0000000000000..7ae3a104fc1ba --- /dev/null +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -0,0 +1,177 @@ +"""Config flow for DLNA DMS.""" +from __future__ import annotations + +import logging +from pprint import pformat +from typing import Any, cast +from urllib.parse import urlparse + +from async_upnp_client.profiles.dlna import DmsDevice +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import ssdp +from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import IntegrationError + +from .const import DEFAULT_NAME, DOMAIN + +LOGGER = logging.getLogger(__name__) + + +class ConnectError(IntegrationError): + """Error occurred when trying to connect to a device.""" + + +class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a DLNA DMS config flow. + + The Unique Service Name (USN) of the DMS device is used as the unique_id for + config entries and for entities. This USN may differ from the root USN if + the DMS is an embedded device. + """ + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + self._discoveries: dict[str, ssdp.SsdpServiceInfo] = {} + self._location: str | None = None + self._usn: str | None = None + self._name: str | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user by listing unconfigured devices.""" + LOGGER.debug("async_step_user: user_input: %s", user_input) + + if user_input is not None and (host := user_input.get(CONF_HOST)): + # User has chosen a device + discovery = self._discoveries[host] + await self._async_parse_discovery(discovery) + return self._create_entry() + + if not (discoveries := await self._async_get_discoveries()): + # Nothing found, abort configuration + return self.async_abort(reason="no_devices_found") + + self._discoveries = { + cast(str, urlparse(discovery.ssdp_location).hostname): discovery + for discovery in discoveries + } + + discovery_choices = { + host: f"{discovery.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)} ({host})" + for host, discovery in self._discoveries.items() + } + data_schema = vol.Schema({vol.Optional(CONF_HOST): vol.In(discovery_choices)}) + return self.async_show_form(step_id="user", data_schema=data_schema) + + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a flow initialized by SSDP discovery.""" + LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) + + await self._async_parse_discovery(discovery_info) + + # Abort if the device doesn't support all services required for a DmsDevice. + # Use the discovery_info instead of DmsDevice.is_profile_device to avoid + # contacting the device again. + discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST) + if not discovery_service_list: + return self.async_abort(reason="not_dms") + discovery_service_ids = { + service.get("serviceId") + for service in discovery_service_list.get("service") or [] + } + if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids): + return self.async_abort(reason="not_dms") + + # Abort if another config entry has the same location, in case the + # device doesn't have a static and unique UDN (breaking the UPnP spec). + self._async_abort_entries_match({CONF_URL: self._location}) + + self.context["title_placeholders"] = {"name": self._name} + + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + LOGGER.debug("async_step_confirm: %s", user_input) + + if user_input is not None: + return self._create_entry() + + self._set_confirm_only() + return self.async_show_form(step_id="confirm") + + def _create_entry(self) -> FlowResult: + """Create a config entry, assuming all required information is now known.""" + LOGGER.debug( + "_async_create_entry: location: %s, USN: %s", self._location, self._usn + ) + assert self._name + assert self._location + assert self._usn + + data = {CONF_URL: self._location, CONF_DEVICE_ID: self._usn} + return self.async_create_entry(title=self._name, data=data) + + async def _async_parse_discovery( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> None: + """Get required details from an SSDP discovery. + + Aborts if a device matching the SSDP USN has already been configured. + """ + LOGGER.debug( + "_async_parse_discovery: location: %s, USN: %s", + discovery_info.ssdp_location, + discovery_info.ssdp_usn, + ) + + if not discovery_info.ssdp_location or not discovery_info.ssdp_usn: + raise AbortFlow("bad_ssdp") + + if not self._location: + self._location = discovery_info.ssdp_location + + self._usn = discovery_info.ssdp_usn + await self.async_set_unique_id(self._usn) + + # Abort if already configured, but update the last-known location + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) + + self._name = ( + discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(self._location).hostname + or DEFAULT_NAME + ) + + async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]: + """Get list of unconfigured DLNA devices discovered by SSDP.""" + LOGGER.debug("_get_discoveries") + + # Get all compatible devices from ssdp's cache + discoveries: list[ssdp.SsdpServiceInfo] = [] + for udn_st in DmsDevice.DEVICE_TYPES: + st_discoveries = await ssdp.async_get_discovery_info_by_st( + self.hass, udn_st + ) + discoveries.extend(st_discoveries) + + # Filter out devices already configured + current_unique_ids = { + entry.unique_id + for entry in self._async_current_entries(include_ignore=False) + } + discoveries = [ + disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids + ] + + return discoveries diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py new file mode 100644 index 0000000000000..8c260272d5fa7 --- /dev/null +++ b/homeassistant/components/dlna_dms/const.py @@ -0,0 +1,78 @@ +"""Constants for the DLNA MediaServer integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Final + +from homeassistant.components.media_player import const as _mp_const + +LOGGER = logging.getLogger(__package__) + +DOMAIN: Final = "dlna_dms" +DEFAULT_NAME: Final = "DLNA Media Server" + +SOURCE_SEP: Final = "/" +ROOT_OBJECT_ID: Final = "0" +PATH_SEP: Final = "/" +PATH_SEARCH_FLAG: Final = "?" +PATH_OBJECT_ID_FLAG: Final = ":" +# Only request the metadata needed to build a browse response +DLNA_BROWSE_FILTER: Final = [ + "id", + "upnp:class", + "dc:title", + "res", + "@childCount", + "upnp:albumArtURI", +] +# Get all metadata when resolving, for the use of media_players +DLNA_RESOLVE_FILTER: Final = "*" +# Metadata needed to resolve a path +DLNA_PATH_FILTER: Final = ["id", "upnp:class", "dc:title"] +DLNA_SORT_CRITERIA: Final = ["+upnp:class", "+upnp:originalTrackNumber", "+dc:title"] + +PROTOCOL_HTTP: Final = "http-get" +PROTOCOL_RTSP: Final = "rtsp-rtp-udp" +PROTOCOL_ANY: Final = "*" +STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] + +# Map UPnP object class to media_player media class +MEDIA_CLASS_MAP: Mapping[str, str] = { + "object": _mp_const.MEDIA_CLASS_URL, + "object.item": _mp_const.MEDIA_CLASS_URL, + "object.item.imageItem": _mp_const.MEDIA_CLASS_IMAGE, + "object.item.imageItem.photo": _mp_const.MEDIA_CLASS_IMAGE, + "object.item.audioItem": _mp_const.MEDIA_CLASS_MUSIC, + "object.item.audioItem.musicTrack": _mp_const.MEDIA_CLASS_MUSIC, + "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_CLASS_MUSIC, + "object.item.audioItem.audioBook": _mp_const.MEDIA_CLASS_PODCAST, + "object.item.videoItem": _mp_const.MEDIA_CLASS_VIDEO, + "object.item.videoItem.movie": _mp_const.MEDIA_CLASS_MOVIE, + "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_CLASS_TV_SHOW, + "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_CLASS_VIDEO, + "object.item.playlistItem": _mp_const.MEDIA_CLASS_TRACK, + "object.item.textItem": _mp_const.MEDIA_CLASS_URL, + "object.item.bookmarkItem": _mp_const.MEDIA_CLASS_URL, + "object.item.epgItem": _mp_const.MEDIA_CLASS_EPISODE, + "object.item.epgItem.audioProgram": _mp_const.MEDIA_CLASS_MUSIC, + "object.item.epgItem.videoProgram": _mp_const.MEDIA_CLASS_VIDEO, + "object.container": _mp_const.MEDIA_CLASS_DIRECTORY, + "object.container.person": _mp_const.MEDIA_CLASS_ARTIST, + "object.container.person.musicArtist": _mp_const.MEDIA_CLASS_ARTIST, + "object.container.playlistContainer": _mp_const.MEDIA_CLASS_PLAYLIST, + "object.container.album": _mp_const.MEDIA_CLASS_ALBUM, + "object.container.album.musicAlbum": _mp_const.MEDIA_CLASS_ALBUM, + "object.container.album.photoAlbum": _mp_const.MEDIA_CLASS_ALBUM, + "object.container.genre": _mp_const.MEDIA_CLASS_GENRE, + "object.container.genre.musicGenre": _mp_const.MEDIA_CLASS_GENRE, + "object.container.genre.movieGenre": _mp_const.MEDIA_CLASS_GENRE, + "object.container.channelGroup": _mp_const.MEDIA_CLASS_CHANNEL, + "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, + "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, + "object.container.epgContainer": _mp_const.MEDIA_CLASS_DIRECTORY, + "object.container.storageSystem": _mp_const.MEDIA_CLASS_DIRECTORY, + "object.container.storageVolume": _mp_const.MEDIA_CLASS_DIRECTORY, + "object.container.storageFolder": _mp_const.MEDIA_CLASS_DIRECTORY, + "object.container.bookmarkFolder": _mp_const.MEDIA_CLASS_DIRECTORY, +} diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py new file mode 100644 index 0000000000000..d7ee08f85f803 --- /dev/null +++ b/homeassistant/components/dlna_dms/dms.py @@ -0,0 +1,749 @@ +"""Wrapper for media_source around async_upnp_client's DmsDevice .""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import functools +from typing import Any, TypeVar, cast + +from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester +from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.const import NotificationSubType +from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError +from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice +from didl_lite import didl_lite + +from homeassistant.backports.enum import StrEnum +from homeassistant.components import ssdp +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import BrowseMediaSource, PlayMedia +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client +from homeassistant.util import slugify + +from .const import ( + DLNA_BROWSE_FILTER, + DLNA_PATH_FILTER, + DLNA_RESOLVE_FILTER, + DLNA_SORT_CRITERIA, + DOMAIN, + LOGGER, + MEDIA_CLASS_MAP, + PATH_OBJECT_ID_FLAG, + PATH_SEARCH_FLAG, + PATH_SEP, + ROOT_OBJECT_ID, + STREAMABLE_PROTOCOLS, +) + +_DlnaDmsDeviceMethod = TypeVar("_DlnaDmsDeviceMethod", bound="DmsDeviceSource") +_RetType = TypeVar("_RetType") + + +class DlnaDmsData: + """Storage class for domain global data.""" + + hass: HomeAssistant + lock: asyncio.Lock + requester: UpnpRequester + upnp_factory: UpnpFactory + event_handler: UpnpEventHandler + devices: dict[str, DmsDeviceSource] # Indexed by config_entry.unique_id + sources: dict[str, DmsDeviceSource] # Indexed by source_id + + def __init__( + self, + hass: HomeAssistant, + ) -> None: + """Initialize global data.""" + self.hass = hass + self.lock = asyncio.Lock() + session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) + self.requester = AiohttpSessionRequester(session, with_sleep=True) + self.upnp_factory = UpnpFactory(self.requester, non_strict=True) + # NOTE: event_handler is not actually used, and is only created to + # satisfy the DmsDevice.__init__ signature + self.event_handler = UpnpEventHandler("", self.requester) + self.devices = {} + self.sources = {} + + async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: + """Create a DMS device connection from a config entry.""" + assert config_entry.unique_id + async with self.lock: + source_id = self._generate_source_id(config_entry.title) + device = DmsDeviceSource(self.hass, config_entry, source_id) + self.devices[config_entry.unique_id] = device + self.sources[device.source_id] = device + + # Update the device when the associated config entry is modified + config_entry.async_on_unload( + config_entry.add_update_listener(self.async_update_entry) + ) + + await device.async_added_to_hass() + return True + + async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: + """Unload a config entry and disconnect the corresponding DMS device.""" + assert config_entry.unique_id + async with self.lock: + device = self.devices.pop(config_entry.unique_id) + del self.sources[device.source_id] + await device.async_will_remove_from_hass() + return True + + async def async_update_entry( + self, hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: + """Update a DMS device when the config entry changes.""" + assert config_entry.unique_id + async with self.lock: + device = self.devices[config_entry.unique_id] + # Update the source_id to match the new name + del self.sources[device.source_id] + device.source_id = self._generate_source_id(config_entry.title) + self.sources[device.source_id] = device + + def _generate_source_id(self, name: str) -> str: + """Generate a unique source ID. + + Caller should hold self.lock when calling this method. + """ + source_id_base = slugify(name) + if source_id_base not in self.sources: + return source_id_base + + tries = 1 + while (suggested_source_id := f"{source_id_base}_{tries}") in self.sources: + tries += 1 + + return suggested_source_id + + +@callback +def get_domain_data(hass: HomeAssistant) -> DlnaDmsData: + """Obtain this integration's domain data, creating it if needed.""" + if DOMAIN in hass.data: + return cast(DlnaDmsData, hass.data[DOMAIN]) + + data = DlnaDmsData(hass) + hass.data[DOMAIN] = data + return data + + +@dataclass +class DidlPlayMedia(PlayMedia): + """Playable media with DIDL metadata.""" + + didl_metadata: didl_lite.DidlObject + + +class DlnaDmsDeviceError(BrowseError, Unresolvable): + """Base for errors raised by DmsDeviceSource. + + Caught by both media_player (BrowseError) and media_source (Unresolvable), + so DmsDeviceSource methods can be used for both browse and resolve + functionality. + """ + + +class DeviceConnectionError(DlnaDmsDeviceError): + """Error occurred with the connection to the server.""" + + +class ActionError(DlnaDmsDeviceError): + """Error when calling a UPnP Action on the device.""" + + +def catch_request_errors( + func: Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]] +) -> Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]]: + """Catch UpnpError errors.""" + + @functools.wraps(func) + async def wrapper(self: _DlnaDmsDeviceMethod, req_param: str) -> _RetType: + """Catch UpnpError errors and check availability before and after request.""" + if not self.available: + LOGGER.warning("Device disappeared when trying to call %s", func.__name__) + raise DeviceConnectionError("DMS is not connected") + + try: + return await func(self, req_param) + except UpnpActionError as err: + LOGGER.debug("Server failure", exc_info=err) + if err.error_code == ContentDirectoryErrorCode.NO_SUCH_OBJECT: + LOGGER.debug("No such object: %s", req_param) + raise ActionError(f"No such object: {req_param}") from err + if err.error_code == ContentDirectoryErrorCode.INVALID_SEARCH_CRITERIA: + LOGGER.debug("Invalid query: %s", req_param) + raise ActionError(f"Invalid query: {req_param}") from err + raise DeviceConnectionError(f"Server failure: {err!r}") from err + except UpnpConnectionError as err: + LOGGER.debug("Server disconnected", exc_info=err) + await self.device_disconnect() + raise DeviceConnectionError(f"Server disconnected: {err!r}") from err + except UpnpError as err: + LOGGER.debug("Server communication failure", exc_info=err) + raise DeviceConnectionError( + f"Server communication failure: {err!r}" + ) from err + + return wrapper + + +class DmsDeviceSource: + """DMS Device wrapper, providing media files as a media_source.""" + + hass: HomeAssistant + config_entry: ConfigEntry + + # Unique slug used for media-source URIs + source_id: str + + # Last known URL for the device, used when adding this wrapper to hass to + # try to connect before SSDP has rediscovered it, or when SSDP discovery + # fails. + location: str | None + + _device_lock: asyncio.Lock # Held when connecting or disconnecting the device + _device: DmsDevice | None = None + + # Only try to connect once when an ssdp:alive advertisement is received + _ssdp_connect_failed: bool = False + + # Track BOOTID in SSDP advertisements for device changes + _bootid: int | None = None + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, source_id: str + ) -> None: + """Initialize a DMS Source.""" + self.hass = hass + self.config_entry = config_entry + self.source_id = source_id + self.location = self.config_entry.data[CONF_URL] + self._device_lock = asyncio.Lock() + + # Callbacks and events + + async def async_added_to_hass(self) -> None: + """Handle addition of this source.""" + + # Try to connect to the last known location, but don't worry if not available + if not self._device and self.location: + try: + await self.device_connect() + except UpnpError as err: + LOGGER.debug("Couldn't connect immediately: %r", err) + + # Get SSDP notifications for only this device + self.config_entry.async_on_unload( + await ssdp.async_register_callback( + self.hass, self.async_ssdp_callback, {"USN": self.usn} + ) + ) + + # async_upnp_client.SsdpListener only reports byebye once for each *UDN* + # (device name) which often is not the USN (service within the device) + # that we're interested in. So also listen for byebye advertisements for + # the UDN, which is reported in the _udn field of the combined_headers. + self.config_entry.async_on_unload( + await ssdp.async_register_callback( + self.hass, + self.async_ssdp_callback, + {"_udn": self.udn, "NTS": NotificationSubType.SSDP_BYEBYE}, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Handle removal of this source.""" + await self.device_disconnect() + + async def async_ssdp_callback( + self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange + ) -> None: + """Handle notification from SSDP of device state change.""" + LOGGER.debug( + "SSDP %s notification of device %s at %s", + change, + info.ssdp_usn, + info.ssdp_location, + ) + + try: + bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_BOOTID] + bootid: int | None = int(bootid_str, 10) + except (KeyError, ValueError): + bootid = None + + if change == ssdp.SsdpChange.UPDATE: + # This is an announcement that bootid is about to change + if self._bootid is not None and self._bootid == bootid: + # Store the new value (because our old value matches) so that we + # can ignore subsequent ssdp:alive messages + try: + next_bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_NEXTBOOTID] + self._bootid = int(next_bootid_str, 10) + except (KeyError, ValueError): + pass + # Nothing left to do until ssdp:alive comes through + return + + if self._bootid is not None and self._bootid != bootid: + # Device has rebooted + # Maybe connection will succeed now + self._ssdp_connect_failed = False + if self._device: + # Drop existing connection and maybe reconnect + await self.device_disconnect() + self._bootid = bootid + + if change == ssdp.SsdpChange.BYEBYE: + # Device is going away + if self._device: + # Disconnect from gone device + await self.device_disconnect() + # Maybe the next alive message will result in a successful connection + self._ssdp_connect_failed = False + + if ( + change == ssdp.SsdpChange.ALIVE + and not self._device + and not self._ssdp_connect_failed + ): + assert info.ssdp_location + self.location = info.ssdp_location + try: + await self.device_connect() + except UpnpError as err: + self._ssdp_connect_failed = True + LOGGER.warning( + "Failed connecting to recently alive device at %s: %r", + self.location, + err, + ) + + # Device connection/disconnection + + async def device_connect(self) -> None: + """Connect to the device now that it's available.""" + LOGGER.debug("Connecting to device at %s", self.location) + + async with self._device_lock: + if self._device: + LOGGER.debug("Trying to connect when device already connected") + return + + if not self.location: + LOGGER.debug("Not connecting because location is not known") + return + + domain_data = get_domain_data(self.hass) + + # Connect to the base UPNP device + upnp_device = await domain_data.upnp_factory.async_create_device( + self.location + ) + + # Create profile wrapper + self._device = DmsDevice(upnp_device, domain_data.event_handler) + + # Update state variables. We don't care if they change, so this is + # only done once, here. + await self._device.async_update() + + async def device_disconnect(self) -> None: + """Destroy connections to the device now that it's not available. + + Also call when removing this device wrapper from hass to clean up connections. + """ + async with self._device_lock: + if not self._device: + LOGGER.debug("Disconnecting from device that's not connected") + return + + LOGGER.debug("Disconnecting from %s", self._device.name) + + self._device = None + + # Device properties + + @property + def available(self) -> bool: + """Device is available when we have a connection to it.""" + return self._device is not None and self._device.profile_device.available + + @property + def usn(self) -> str: + """Get the USN (Unique Service Name) for the wrapped UPnP device end-point.""" + return self.config_entry.data[CONF_DEVICE_ID] + + @property + def udn(self) -> str: + """Get the UDN (Unique Device Name) based on the USN.""" + return self.usn.partition("::")[0] + + @property + def name(self) -> str: + """Return a name for the media server.""" + return self.config_entry.title + + @property + def icon(self) -> str | None: + """Return an URL to an icon for the media server.""" + if not self._device: + return None + + return self._device.icon + + # MediaSource methods + + async def async_resolve_media(self, identifier: str) -> DidlPlayMedia: + """Resolve a media item to a playable item.""" + LOGGER.debug("async_resolve_media(%s)", identifier) + action, parameters = _parse_identifier(identifier) + + if action is Action.OBJECT: + return await self.async_resolve_object(parameters) + + if action is Action.PATH: + object_id = await self.async_resolve_path(parameters) + return await self.async_resolve_object(object_id) + + if action is Action.SEARCH: + return await self.async_resolve_search(parameters) + + LOGGER.debug("Invalid identifier %s", identifier) + raise Unresolvable(f"Invalid identifier {identifier}") + + async def async_browse_media(self, identifier: str | None) -> BrowseMediaSource: + """Browse media.""" + LOGGER.debug("async_browse_media(%s)", identifier) + action, parameters = _parse_identifier(identifier) + + if action is Action.OBJECT: + return await self.async_browse_object(parameters) + + if action is Action.PATH: + object_id = await self.async_resolve_path(parameters) + return await self.async_browse_object(object_id) + + if action is Action.SEARCH: + return await self.async_browse_search(parameters) + + return await self.async_browse_object(ROOT_OBJECT_ID) + + # DMS methods + + @catch_request_errors + async def async_resolve_object(self, object_id: str) -> DidlPlayMedia: + """Return a playable media item specified by ObjectID.""" + assert self._device + + item = await self._device.async_browse_metadata( + object_id, metadata_filter=DLNA_RESOLVE_FILTER + ) + + # Use the first playable resource + return self._didl_to_play_media(item) + + @catch_request_errors + async def async_resolve_path(self, path: str) -> str: + """Return an Object ID resolved from a path string.""" + assert self._device + + # Iterate through the path, searching for a matching title within the + # DLNA object hierarchy. + object_id = ROOT_OBJECT_ID + for node in path.split(PATH_SEP): + if not node: + # Skip empty names, for when multiple slashes are involved, e.g // + continue + + criteria = ( + f'@parentID="{_esc_quote(object_id)}" and dc:title="{_esc_quote(node)}"' + ) + try: + result = await self._device.async_search_directory( + object_id, + search_criteria=criteria, + metadata_filter=DLNA_PATH_FILTER, + requested_count=1, + ) + except UpnpActionError as err: + LOGGER.debug("Error in call to async_search_directory: %r", err) + if err.error_code == ContentDirectoryErrorCode.NO_SUCH_CONTAINER: + raise Unresolvable(f"No such container: {object_id}") from err + # Search failed, but can still try browsing children + else: + if result.total_matches > 1: + raise Unresolvable(f"Too many items found for {node} in {path}") + + if result.result: + object_id = result.result[0].id + continue + + # Nothing was found via search, fall back to iterating children + result = await self._device.async_browse_direct_children( + object_id, metadata_filter=DLNA_PATH_FILTER + ) + + if result.total_matches == 0 or not result.result: + raise Unresolvable(f"No contents for {node} in {path}") + + node_lower = node.lower() + for child in result.result: + if child.title.lower() == node_lower: + object_id = child.id + break + else: + # Examining all direct children failed too + raise Unresolvable(f"Nothing found for {node} in {path}") + return object_id + + @catch_request_errors + async def async_resolve_search(self, query: str) -> DidlPlayMedia: + """Return first playable media item found by the query string.""" + assert self._device + + result = await self._device.async_search_directory( + container_id=ROOT_OBJECT_ID, + search_criteria=query, + metadata_filter=DLNA_RESOLVE_FILTER, + requested_count=1, + ) + + if result.total_matches == 0 or not result.result: + raise Unresolvable(f"Nothing found for {query}") + + # Use the first result, even if it doesn't have a playable resource + item = result.result[0] + + if not isinstance(item, didl_lite.DidlObject): + raise Unresolvable(f"{item} is not a DidlObject") + + return self._didl_to_play_media(item) + + @catch_request_errors + async def async_browse_object(self, object_id: str) -> BrowseMediaSource: + """Return the contents of a DLNA container by ObjectID.""" + assert self._device + + base_object = await self._device.async_browse_metadata( + object_id, metadata_filter=DLNA_BROWSE_FILTER + ) + + children = await self._device.async_browse_direct_children( + object_id, + metadata_filter=DLNA_BROWSE_FILTER, + sort_criteria=self._sort_criteria, + ) + + return self._didl_to_media_source(base_object, children) + + @catch_request_errors + async def async_browse_search(self, query: str) -> BrowseMediaSource: + """Return all media items found by the query string.""" + assert self._device + + result = await self._device.async_search_directory( + container_id=ROOT_OBJECT_ID, + search_criteria=query, + metadata_filter=DLNA_BROWSE_FILTER, + ) + + children = [ + self._didl_to_media_source(child) + for child in result.result + if isinstance(child, didl_lite.DidlObject) + ] + + media_source = BrowseMediaSource( + domain=DOMAIN, + identifier=self._make_identifier(Action.SEARCH, query), + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title="Search results", + can_play=False, + can_expand=True, + children=children, + ) + + if media_source.children: + media_source.calculate_children_class() + + return media_source + + def _didl_to_play_media(self, item: didl_lite.DidlObject) -> DidlPlayMedia: + """Return the first playable resource from a DIDL-Lite object.""" + assert self._device + + if not item.res: + LOGGER.debug("Object %s has no resources", item.id) + raise Unresolvable("Object has no resources") + + for resource in item.res: + if not resource.uri: + continue + if mime_type := _resource_mime_type(resource): + url = self._device.get_absolute_url(resource.uri) + LOGGER.debug("Resolved to url %s MIME %s", url, mime_type) + return DidlPlayMedia(url, mime_type, item) + + LOGGER.debug("Object %s has no playable resources", item.id) + raise Unresolvable("Object has no playable resources") + + def _didl_to_media_source( + self, + item: didl_lite.DidlObject, + browsed_children: DmsDevice.BrowseResult | None = None, + ) -> BrowseMediaSource: + """Convert a DIDL-Lite object to a browse media source.""" + children: list[BrowseMediaSource] | None = None + + if browsed_children: + children = [ + self._didl_to_media_source(child) + for child in browsed_children.result + if isinstance(child, didl_lite.DidlObject) + ] + + # Can expand if it has children (even if we don't have them yet), or its + # a container type. Otherwise the front-end will try to play it (even if + # can_play is False). + try: + child_count = int(item.child_count) + except (AttributeError, TypeError, ValueError): + child_count = 0 + can_expand = ( + bool(children) or child_count > 0 or isinstance(item, didl_lite.Container) + ) + + # Can play if item has any resource that can be streamed over the network + can_play = any(_resource_is_streaming(res) for res in item.res) + + # Use server name for root object, not "root" + title = self.name if item.id == ROOT_OBJECT_ID else item.title + + mime_type = _resource_mime_type(item.res[0]) if item.res else None + media_content_type = mime_type or item.upnp_class + + media_source = BrowseMediaSource( + domain=DOMAIN, + identifier=self._make_identifier(Action.OBJECT, item.id), + media_class=MEDIA_CLASS_MAP.get(item.upnp_class, ""), + media_content_type=media_content_type, + title=title, + can_play=can_play, + can_expand=can_expand, + children=children, + thumbnail=self._didl_thumbnail_url(item), + ) + + if media_source.children: + media_source.calculate_children_class() + + return media_source + + def _didl_thumbnail_url(self, item: didl_lite.DidlObject) -> str | None: + """Return absolute URL of a thumbnail for a DIDL-Lite object. + + Some objects have the thumbnail in albumArtURI, others in an image + resource. + """ + assert self._device + + # Based on DmrDevice.media_image_url from async_upnp_client. + if album_art_uri := getattr(item, "album_art_uri", None): + return self._device.get_absolute_url(album_art_uri) + + for resource in item.res: + if not resource.protocol_info or not resource.uri: + continue + if resource.protocol_info.startswith("http-get:*:image/"): + return self._device.get_absolute_url(resource.uri) + + return None + + def _make_identifier(self, action: Action, object_id: str) -> str: + """Make an identifier for BrowseMediaSource.""" + return f"{self.source_id}/{action}{object_id}" + + @property # type: ignore + @functools.cache + def _sort_criteria(self) -> list[str]: + """Return criteria to be used for sorting results. + + The device must be connected before reading this property. + """ + assert self._device + + if self._device.sort_capabilities == ["*"]: + return DLNA_SORT_CRITERIA + + # Filter criteria based on what the device supports. Strings in + # DLNA_SORT_CRITERIA are prefixed with a sign, while those in + # the device's sort_capabilities are not. + return [ + criterion + for criterion in DLNA_SORT_CRITERIA + if criterion[1:] in self._device.sort_capabilities + ] + + +class Action(StrEnum): + """Actions that can be specified in a DMS media-source identifier.""" + + OBJECT = PATH_OBJECT_ID_FLAG + PATH = PATH_SEP + SEARCH = PATH_SEARCH_FLAG + + +def _parse_identifier(identifier: str | None) -> tuple[Action | None, str]: + """Parse the media identifier component of a media-source URI.""" + if not identifier: + return None, "" + if identifier.startswith(PATH_OBJECT_ID_FLAG): + return Action.OBJECT, identifier[1:] + if identifier.startswith(PATH_SEP): + return Action.PATH, identifier[1:] + if identifier.startswith(PATH_SEARCH_FLAG): + return Action.SEARCH, identifier[1:] + return Action.PATH, identifier + + +def _resource_is_streaming(resource: didl_lite.Resource) -> bool: + """Determine if a resource can be streamed across a network.""" + # Err on the side of "True" if the protocol info is not available + if not resource.protocol_info: + return True + protocol = resource.protocol_info.split(":")[0].lower() + return protocol.lower() in STREAMABLE_PROTOCOLS + + +def _resource_mime_type(resource: didl_lite.Resource) -> str | None: + """Return the MIME type of a resource, if specified.""" + # This is the contentFormat portion of the ProtocolInfo for an http-get stream + if not resource.protocol_info: + return None + try: + protocol, _, content_format, _ = resource.protocol_info.split(":", 3) + except ValueError: + return None + if protocol.lower() in STREAMABLE_PROTOCOLS: + return content_format + return None + + +def _esc_quote(contents: str) -> str: + """Escape string contents for DLNA search quoted values. + + See ContentDirectory:v4, section 4.1.2. + """ + return contents.replace("\\", "\\\\").replace('"', '\\"') diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json new file mode 100644 index 0000000000000..84cfc2e69fdc2 --- /dev/null +++ b/homeassistant/components/dlna_dms/manifest.json @@ -0,0 +1,30 @@ +{ + "domain": "dlna_dms", + "name": "DLNA Digital Media Server", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dlna_dms", + "requirements": ["async-upnp-client==0.23.5"], + "dependencies": ["ssdp"], + "after_dependencies": ["media_source"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", + "st": "urn:schemas-upnp-org:device:MediaServer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:2", + "st": "urn:schemas-upnp-org:device:MediaServer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:3", + "st": "urn:schemas-upnp-org:device:MediaServer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:4", + "st": "urn:schemas-upnp-org:device:MediaServer:4" + } + ], + "codeowners": ["@chishm"], + "iot_class": "local_polling", + "quality_scale": "platinum" +} diff --git a/homeassistant/components/dlna_dms/media_source.py b/homeassistant/components/dlna_dms/media_source.py new file mode 100644 index 0000000000000..84910b7ff67ef --- /dev/null +++ b/homeassistant/components/dlna_dms/media_source.py @@ -0,0 +1,126 @@ +"""Implementation of DLNA DMS as a media source. + +URIs look like "media-source://dlna_dms//" + +Media identifiers can look like: +* `/path/to/file`: slash-separated path through the Content Directory +* `:ObjectID`: colon followed by a server-assigned ID for an object +* `?query`: question mark followed by a query string to search for, + see [DLNA ContentDirectory SearchCriteria](http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf) + for the syntax. +""" + +from __future__ import annotations + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, +) +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, LOGGER, PATH_OBJECT_ID_FLAG, ROOT_OBJECT_ID, SOURCE_SEP +from .dms import DidlPlayMedia, get_domain_data + + +async def async_get_media_source(hass: HomeAssistant): + """Set up DLNA DMS media source.""" + LOGGER.debug("Setting up DLNA media sources") + return DmsMediaSource(hass) + + +class DmsMediaSource(MediaSource): + """Provide DLNA Media Servers as media sources.""" + + name = "DLNA Servers" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize DLNA source.""" + super().__init__(DOMAIN) + + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> DidlPlayMedia: + """Resolve a media item to a playable item.""" + dms_data = get_domain_data(self.hass) + if not dms_data.sources: + raise Unresolvable("No sources have been configured") + + source_id, media_id = _parse_identifier(item) + if not source_id: + raise Unresolvable(f"No source ID in {item.identifier}") + if not media_id: + raise Unresolvable(f"No media ID in {item.identifier}") + + try: + source = dms_data.sources[source_id] + except KeyError as err: + raise Unresolvable(f"Unknown source ID: {source_id}") from err + + return await source.async_resolve_media(media_id) + + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: + """Browse media.""" + dms_data = get_domain_data(self.hass) + if not dms_data.sources: + raise BrowseError("No sources have been configured") + + source_id, media_id = _parse_identifier(item) + LOGGER.debug("Browsing for %s / %s", source_id, media_id) + + if not source_id and len(dms_data.sources) > 1: + # Browsing the root of dlna_dms with more than one server, return + # all known servers. + base = BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_CHANNELS, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_CHANNEL, + ) + + base.children = [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{source_id}/{PATH_OBJECT_ID_FLAG}{ROOT_OBJECT_ID}", + media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MEDIA_TYPE_CHANNEL, + title=source.name, + can_play=False, + can_expand=True, + thumbnail=source.icon, + ) + for source_id, source in dms_data.sources.items() + ] + + return base + + if not source_id: + # No source specified, default to the first registered + source_id = next(iter(dms_data.sources)) + + try: + source = dms_data.sources[source_id] + except KeyError as err: + raise BrowseError(f"Unknown source ID: {source_id}") from err + + return await source.async_browse_media(media_id) + + +def _parse_identifier(item: MediaSourceItem) -> tuple[str | None, str | None]: + """Parse the source_id and media identifier from a media source item.""" + if not item.identifier: + return None, None + source_id, _, media_id = item.identifier.partition(SOURCE_SEP) + return source_id or None, media_id or None diff --git a/homeassistant/components/dlna_dms/strings.json b/homeassistant/components/dlna_dms/strings.json new file mode 100644 index 0000000000000..9b59960a78af4 --- /dev/null +++ b/homeassistant/components/dlna_dms/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "title": "Discovered DLNA DMA devices", + "description": "Choose a device to configure", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "bad_ssdp": "SSDP data is missing a required value", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "not_dms": "Device is not a supported Media Server" + } + } +} diff --git a/homeassistant/components/dlna_dms/translations/ca.json b/homeassistant/components/dlna_dms/translations/ca.json new file mode 100644 index 0000000000000..f5cdd4cc4415c --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "bad_ssdp": "Falta un valor necessari a les dades SSDP", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_dms": "El dispositiu no \u00e9s un servidor multim\u00e8dia compatible" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Tria un dispositiu a configurar", + "title": "Dispositius DLNA DMA descoberts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/de.json b/homeassistant/components/dlna_dms/translations/de.json new file mode 100644 index 0000000000000..ebe1946707ef9 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "bad_ssdp": "In den SSDP-Daten fehlt ein erforderlicher Wert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_dms": "Das Ger\u00e4t ist kein unterst\u00fctzter Medienserver" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "W\u00e4hle ein zu konfigurierendes Ger\u00e4t aus", + "title": "Erkannte DLNA DMA-Ger\u00e4te" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/el.json b/homeassistant/components/dlna_dms/translations/el.json new file mode 100644 index 0000000000000..94d14f7536061 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/el.json @@ -0,0 +1,24 @@ +{ + "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", + "bad_ssdp": "\u0391\u03c0\u03cc \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 SSDP \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c4\u03b9\u03bc\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", + "not_dms": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" + }, + "flow_title": "{name}", + "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;" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 DLNA DMA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/en.json b/homeassistant/components/dlna_dms/translations/en.json new file mode 100644 index 0000000000000..6d07a25a27d27 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "bad_ssdp": "SSDP data is missing a required value", + "no_devices_found": "No devices found on the network", + "not_dms": "Device is not a supported Media Server" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Choose a device to configure", + "title": "Discovered DLNA DMA devices" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/et.json b/homeassistant/components/dlna_dms/translations/et.json new file mode 100644 index 0000000000000..744d4ad54a71b --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "bad_ssdp": "SSDP andmetes puudub n\u00f5utav v\u00e4\u00e4rtus", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "not_dms": "Seade ei ole toetatud meediaserver" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Vali h\u00e4\u00e4lestatav seade", + "title": "Avastatud DLNA DMA-seadmed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/hu.json b/homeassistant/components/dlna_dms/translations/hu.json new file mode 100644 index 0000000000000..8c645d42aa876 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/hu.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "bad_ssdp": "Az SSDP-adatok hi\u00e1nyosak", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_dms": "A m\u00e9diaszerver eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "V\u00e1lasszon egy konfigur\u00e1land\u00f3 eszk\u00f6zt", + "title": "Felfedezett DLNA DMA eszk\u00f6z\u00f6k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/ja.json b/homeassistant/components/dlna_dms/translations/ja.json new file mode 100644 index 0000000000000..c7f8f5c1587ca --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "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", + "bad_ssdp": "SSDP\u30c7\u30fc\u30bf\u306b\u5fc5\u8981\u306a\u5024\u304c\u3042\u308a\u307e\u305b\u3093", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_dms": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u30e1\u30c7\u30a3\u30a2\u30b5\u30fc\u30d0\u30fc\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", + "title": "\u691c\u51fa\u3055\u308c\u305fDLNA DMA\u30c7\u30d0\u30a4\u30b9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/no.json b/homeassistant/components/dlna_dms/translations/no.json new file mode 100644 index 0000000000000..3b36e3c8b3aba --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "bad_ssdp": "SSDP-data mangler en n\u00f8dvendig verdi", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_dms": "Enheten er ikke en st\u00f8ttet medieserver" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Velg en enhet \u00e5 konfigurere", + "title": "Oppdaget DLNA DMA-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pl.json b/homeassistant/components/dlna_dms/translations/pl.json new file mode 100644 index 0000000000000..bd7407f80b689 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "bad_ssdp": "W danych SSDP brakuje wymaganej warto\u015bci", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_dms": "Urz\u0105dzenie nie jest obs\u0142ugiwanym serwerem multimedi\u00f3w" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania", + "title": "Wykryto urz\u0105dzenia DLNA DMA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt-BR.json b/homeassistant/components/dlna_dms/translations/pt-BR.json new file mode 100644 index 0000000000000..125a31fe9b5fb --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "bad_ssdp": "Falta um valor obrigat\u00f3rio nos dados SSDP", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_dms": "O dispositivo n\u00e3o \u00e9 um servidor de m\u00eddia compat\u00edvel" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Escolha um dispositivo para configurar", + "title": "Dispositivos DLNA DMA descobertos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/ru.json b/homeassistant/components/dlna_dms/translations/ru.json new file mode 100644 index 0000000000000..52a8ad0ee1437 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "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.", + "bad_ssdp": "\u0412 \u0434\u0430\u043d\u043d\u044b\u0445 SSDP \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "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_dms": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Media Server." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "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.", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json new file mode 100644 index 0000000000000..2f06619c00602 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "bad_ssdp": "\u6240\u7f3a\u5c11\u7684 SSDP \u8cc7\u6599\u70ba\u5fc5\u9808\u6578\u503c", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_dms": "\u88dd\u7f6e\u4e26\u975e\u652f\u63f4\u5a92\u9ad4\u4f3a\u670d\u5668" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", + "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMA \u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/ca.json b/homeassistant/components/dnsip/translations/ca.json index 813dcdd694f17..f84a0e0a64307 100644 --- a/homeassistant/components/dnsip/translations/ca.json +++ b/homeassistant/components/dnsip/translations/ca.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "El nom d'amfitri\u00f3 al qual realitzar la consulta DNS" + "hostname": "El nom d'amfitri\u00f3 al qual realitzar la consulta DNS", + "resolver": "Resolutor de cerca IPV4", + "resolver_ipv6": "Resolutor de cerca IPV6" } } } diff --git a/homeassistant/components/dnsip/translations/de.json b/homeassistant/components/dnsip/translations/de.json index 76aef3a035d97..9d82d5c76551c 100644 --- a/homeassistant/components/dnsip/translations/de.json +++ b/homeassistant/components/dnsip/translations/de.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Der Hostname, f\u00fcr den die DNS-Abfrage durchgef\u00fchrt werden soll" + "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" } } } diff --git a/homeassistant/components/dnsip/translations/el.json b/homeassistant/components/dnsip/translations/el.json index 7cdf7f885e788..20dd21e72e21c 100644 --- a/homeassistant/components/dnsip/translations/el.json +++ b/homeassistant/components/dnsip/translations/el.json @@ -2,6 +2,28 @@ "config": { "error": { "invalid_hostname": "\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" + }, + "step": { + "user": { + "data": { + "hostname": "\u03a4\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 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03b5\u03af \u03c4\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 DNS", + "resolver": "\u0395\u03c0\u03b9\u03bb\u03cd\u03c4\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IPV4", + "resolver_ipv6": "\u0395\u03c0\u03b9\u03bb\u03cd\u03c4\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IPV6" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03cd\u03c4\u03b7" + }, + "step": { + "init": { + "data": { + "resolver": "\u0395\u03c0\u03b9\u03bb\u03cd\u03c4\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IPV4", + "resolver_ipv6": "\u0395\u03c0\u03b9\u03bb\u03cd\u03c4\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IPV6" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/es.json b/homeassistant/components/dnsip/translations/es.json new file mode 100644 index 0000000000000..a5a51b76746db --- /dev/null +++ b/homeassistant/components/dnsip/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "Nombre de host inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "hostname": "El nombre de host para el que se realiza la consulta DNS", + "resolver": "Conversor para la b\u00fasqueda de IPV4", + "resolver_ipv6": "Conversor para la b\u00fasqueda de IPV6" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Direcci\u00f3n IP no v\u00e1lida para resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver para la b\u00fasqueda de IPV4", + "resolver_ipv6": "Resolver para la b\u00fasqueda de IPV6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/et.json b/homeassistant/components/dnsip/translations/et.json index 7518a12d2007e..f49e83e9b2a34 100644 --- a/homeassistant/components/dnsip/translations/et.json +++ b/homeassistant/components/dnsip/translations/et.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Hostnimi mille kohta DNS p\u00e4ring tehakse" + "hostname": "Hostnimi mille kohta DNS p\u00e4ring tehakse", + "resolver": "IPV4 otsingu lahendaja", + "resolver_ipv6": "IPV6 otsingu lahendaja" } } } diff --git a/homeassistant/components/dnsip/translations/fr.json b/homeassistant/components/dnsip/translations/fr.json index fb3e4a5f6ab4e..ae6da0296c25c 100644 --- a/homeassistant/components/dnsip/translations/fr.json +++ b/homeassistant/components/dnsip/translations/fr.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Le nom d'h\u00f4te pour lequel la requ\u00eate DNS doit \u00eatre effectu\u00e9e." + "hostname": "Le nom d'h\u00f4te pour lequel la requ\u00eate DNS doit \u00eatre effectu\u00e9e.", + "resolver": "R\u00e9solveur pour la recherche IPV4", + "resolver_ipv6": "R\u00e9solveur pour la recherche IPV6" } } } diff --git a/homeassistant/components/dnsip/translations/hu.json b/homeassistant/components/dnsip/translations/hu.json index e9dbb39a609ea..bb60366aa86c6 100644 --- a/homeassistant/components/dnsip/translations/hu.json +++ b/homeassistant/components/dnsip/translations/hu.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "A gazdag\u00e9pn\u00e9v, amelyhez a DNS-lek\u00e9rdez\u00e9st v\u00e9gre kell hajtani" + "hostname": "A gazdag\u00e9pn\u00e9v, amelyhez a DNS-lek\u00e9rdez\u00e9st v\u00e9gre kell hajtani", + "resolver": "Felold\u00f3 az IPV4-keres\u00e9shez", + "resolver_ipv6": "Felold\u00f3 az IPV6-keres\u00e9shez" } } } diff --git a/homeassistant/components/dnsip/translations/id.json b/homeassistant/components/dnsip/translations/id.json new file mode 100644 index 0000000000000..23313013af453 --- /dev/null +++ b/homeassistant/components/dnsip/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "Nama host tidak valid" + }, + "step": { + "user": { + "data": { + "hostname": "Nama host untuk melakukan kueri DNS", + "resolver": "Resolver untuk pencarian IPV4", + "resolver_ipv6": "Resolver untuk pencarian IPV6" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Alamat IP tidak valid untuk resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver untuk pencarian IPV4", + "resolver_ipv6": "Resolver untuk pencarian IPV6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/it.json b/homeassistant/components/dnsip/translations/it.json index 30ca953b2438d..2ed18baa17864 100644 --- a/homeassistant/components/dnsip/translations/it.json +++ b/homeassistant/components/dnsip/translations/it.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Il nome host per il quale eseguire la query DNS" + "hostname": "Il nome host per il quale eseguire la query DNS", + "resolver": "Risolutore per la ricerca IPV4", + "resolver_ipv6": "Risolutore per la ricerca IPV6" } } } diff --git a/homeassistant/components/dnsip/translations/ja.json b/homeassistant/components/dnsip/translations/ja.json index bf252c8ef80e6..6b23c26b2b20d 100644 --- a/homeassistant/components/dnsip/translations/ja.json +++ b/homeassistant/components/dnsip/translations/ja.json @@ -2,6 +2,15 @@ "config": { "error": { "invalid_hostname": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d" + }, + "step": { + "user": { + "data": { + "hostname": "DNS\u30af\u30a8\u30ea\u3092\u5b9f\u884c\u3059\u308b\u30db\u30b9\u30c8\u540d", + "resolver": "IPV4\u30eb\u30c3\u30af\u30a2\u30c3\u30d7\u7528\u306e\u30ea\u30be\u30eb\u30d0\u30fc", + "resolver_ipv6": "IPV6\u30eb\u30c3\u30af\u30a2\u30c3\u30d7\u7528\u306e\u30ea\u30be\u30eb\u30d0\u30fc" + } + } } }, "options": { diff --git a/homeassistant/components/dnsip/translations/nl.json b/homeassistant/components/dnsip/translations/nl.json index b0aebece7b439..99de016dace99 100644 --- a/homeassistant/components/dnsip/translations/nl.json +++ b/homeassistant/components/dnsip/translations/nl.json @@ -6,7 +6,22 @@ "step": { "user": { "data": { - "hostname": "De hostnaam waarvoor de DNS query moet worden uitgevoerd" + "hostname": "De hostnaam waarvoor de DNS query moet worden uitgevoerd", + "resolver": "Resolver voor IPV4 lookup", + "resolver_ipv6": "Resolver voor IPV6 lookup" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Ongeldig IP-adres voor resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver voor IPV4 lookup", + "resolver_ipv6": "Resolver voor IPV6 lookup" } } } diff --git a/homeassistant/components/dnsip/translations/no.json b/homeassistant/components/dnsip/translations/no.json index e99d67902e0d9..ef665c89805c3 100644 --- a/homeassistant/components/dnsip/translations/no.json +++ b/homeassistant/components/dnsip/translations/no.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Vertsnavnet som DNS-sp\u00f8rringen skal utf\u00f8res for" + "hostname": "Vertsnavnet som DNS-sp\u00f8rringen skal utf\u00f8res for", + "resolver": "L\u00f8ser for IPV4-oppslag", + "resolver_ipv6": "L\u00f8ser for IPV6-oppslag" } } } diff --git a/homeassistant/components/dnsip/translations/pl.json b/homeassistant/components/dnsip/translations/pl.json index 5a7d7a4bff010..f67e5bbfbee52 100644 --- a/homeassistant/components/dnsip/translations/pl.json +++ b/homeassistant/components/dnsip/translations/pl.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "Nazwa hosta, dla kt\u00f3rego ma zosta\u0107 wykonane zapytanie DNS" + "hostname": "Nazwa hosta, dla kt\u00f3rego ma zosta\u0107 wykonane zapytanie DNS", + "resolver": "Program do rozpoznawania nazw dla wyszukiwania IPV4", + "resolver_ipv6": "Program do rozpoznawania nazw dla wyszukiwania IPV6" } } } diff --git a/homeassistant/components/dnsip/translations/pt-BR.json b/homeassistant/components/dnsip/translations/pt-BR.json new file mode 100644 index 0000000000000..3d294a43d3132 --- /dev/null +++ b/homeassistant/components/dnsip/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "hostname": "O hostname para o qual realizar a consulta DNS", + "resolver": "Resolvedor para consulta IPV4", + "resolver_ipv6": "Resolvedor para consulta IPV6" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Endere\u00e7o IP inv\u00e1lido para resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver para a busca ipv4", + "resolver_ipv6": "Resolver para a busca IPV6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/ru.json b/homeassistant/components/dnsip/translations/ru.json index 3ced95d27ff82..a4421b566f60f 100644 --- a/homeassistant/components/dnsip/translations/ru.json +++ b/homeassistant/components/dnsip/translations/ru.json @@ -6,7 +6,9 @@ "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": "\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", + "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/tr.json b/homeassistant/components/dnsip/translations/tr.json index 95f0c45bb6b17..d53abc4fc135c 100644 --- a/homeassistant/components/dnsip/translations/tr.json +++ b/homeassistant/components/dnsip/translations/tr.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "DNS sorgusunun ger\u00e7ekle\u015ftirilece\u011fi ana bilgisayar ad\u0131" + "hostname": "DNS sorgusunun ger\u00e7ekle\u015ftirilece\u011fi ana bilgisayar ad\u0131", + "resolver": "IPV4 aramas\u0131 i\u00e7in \u00e7\u00f6z\u00fcmleyici", + "resolver_ipv6": "IPV6 aramas\u0131 i\u00e7in \u00e7\u00f6z\u00fcmleyici" } } } diff --git a/homeassistant/components/dnsip/translations/zh-Hans.json b/homeassistant/components/dnsip/translations/zh-Hans.json new file mode 100644 index 0000000000000..780b8f817538a --- /dev/null +++ b/homeassistant/components/dnsip/translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "\u65e0\u6548\u7684\u57df\u540d\u6216\u4e3b\u673a\u540d" + }, + "step": { + "user": { + "data": { + "hostname": "\u8bf7\u952e\u5165\u60a8\u60f3\u8981\u6267\u884c DNS \u67e5\u8be2\u7684\u57df\u540d\u6216\u4e3b\u673a\u540d", + "resolver": "IPv4 DNS \u89e3\u6790\u670d\u52a1\u5668", + "resolver_ipv6": "IPv6 DNS \u89e3\u6790\u670d\u52a1\u5668" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "DNS \u89e3\u6790\u670d\u52a1\u5668 IP \u5730\u5740\u65e0\u6548" + }, + "step": { + "init": { + "data": { + "resolver": "IPv4 DNS \u89e3\u6790\u670d\u52a1\u5668", + "resolver_ipv6": "IPv6 DNS \u89e3\u6790\u670d\u52a1\u5668" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/zh-Hant.json b/homeassistant/components/dnsip/translations/zh-Hant.json index 975dacd5c2aef..5c46b1b0282ac 100644 --- a/homeassistant/components/dnsip/translations/zh-Hant.json +++ b/homeassistant/components/dnsip/translations/zh-Hant.json @@ -6,7 +6,9 @@ "step": { "user": { "data": { - "hostname": "\u57f7\u884c DNS \u67e5\u8a62\u7684\u4e3b\u6a5f\u540d\u7a31" + "hostname": "\u57f7\u884c DNS \u67e5\u8a62\u7684\u4e3b\u6a5f\u540d\u7a31", + "resolver": "IPV4 \u89e3\u6790\u5668", + "resolver_ipv6": "IPV6 \u89e3\u6790\u5668" } } } diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index d7d366befd4b4..48b02cb97951a 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pizzapi==0.0.3"], "dependencies": ["http"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pizzapi"] } diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 86ad7ae4a90ce..a8be4e4fcdb2b 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "requirements": ["pydoods==1.0.2", "pillow==9.0.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pydoods"] } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 1018d23fd8f4f..502ff453a2744 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,6 +3,7 @@ from http import HTTPStatus import logging +from typing import Any from aiohttp import web from doorbirdpy import DoorBird @@ -166,7 +167,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _async_register_events(hass, doorstation): +async def _async_register_events( + hass: HomeAssistant, doorstation: ConfiguredDoorBird +) -> bool: try: await hass.async_add_executor_job(doorstation.register_events, hass) except requests.exceptions.HTTPError: @@ -184,7 +187,7 @@ async def _async_register_events(hass, doorstation): return True -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" config_entry_id = entry.entry_id doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] @@ -243,7 +246,7 @@ def token(self): """Get token for device.""" return self._token - def register_events(self, hass): + def register_events(self, hass: HomeAssistant) -> None: """Register events on device.""" # Get the URL of this server hass_url = get_url(hass) @@ -258,9 +261,10 @@ def register_events(self, hass): favorites = self.device.favorites() for event in self.doorstation_events: - self._register_event(hass_url, event, favs=favorites) - - _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) + if self._register_event(hass_url, event, favs=favorites): + _LOGGER.info( + "Successfully registered URL for %s on %s", event, self.name + ) @property def slug(self): @@ -270,21 +274,25 @@ def slug(self): def _get_event_name(self, event): return f"{self.slug}_{event}" - def _register_event(self, hass_url, event, favs=None): + def _register_event( + self, hass_url: str, event: str, favs: dict[str, Any] | None = None + ) -> bool: """Add a schedule entry in the device for a sensor.""" url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID if self.webhook_is_registered(url, favs=favs): - return + return True self.device.change_favorite("http", f"Home Assistant ({event})", url) if not self.webhook_is_registered(url): _LOGGER.warning( - 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', + 'Unable to set favorite URL "%s". ' 'Event "%s" will not fire', url, event, ) + return False + return True def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 08c77f048a0bb..6fc29343d0408 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -12,5 +12,6 @@ ], "codeowners": ["@oblogic7", "@bdraco", "@flacjacket"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["doorbirdpy"] } diff --git a/homeassistant/components/doorbird/translations/el.json b/homeassistant/components/doorbird/translations/el.json index b01bd3d8e8ed0..ffbf523e7d8c8 100644 --- a/homeassistant/components/doorbird/translations/el.json +++ b/homeassistant/components/doorbird/translations/el.json @@ -1,7 +1,24 @@ { "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", + "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", + "not_doorbird_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 DoorBird" + }, + "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\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} ({host})", "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 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\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" + }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf DoorBird" } } diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 4a39ef36f3c3d..83f1ab9c5ebfa 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -19,7 +19,7 @@ "password": "Password", "username": "Nome utente" }, - "title": "Connetti a DoorBird" + "title": "Connettiti a DoorBird" } } }, diff --git a/homeassistant/components/doorbird/translations/pt-BR.json b/homeassistant/components/doorbird/translations/pt-BR.json index 828f6a24e8469..3f2c479df8f0b 100644 --- a/homeassistant/components/doorbird/translations/pt-BR.json +++ b/homeassistant/components/doorbird/translations/pt-BR.json @@ -1,9 +1,36 @@ { "config": { "abort": { - "already_configured": "Este DoorBird j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", "not_doorbird_device": "Este dispositivo n\u00e3o \u00e9 um DoorBird" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome do dispositivo", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se ao DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Lista de eventos separados por v\u00edrgulas." + }, + "description": "Adicione um nome de evento separado por v\u00edrgula para cada evento que voc\u00ea deseja rastrear. Depois de inseri-los aqui, use o aplicativo DoorBird para atribu\u00ed-los a um evento espec\u00edfico. Consulte a documenta\u00e7\u00e3o em https://www.home-assistant.io/integrations/doorbird/#events. Exemplo: alguem_pressionou_o_botao movimento" + } } } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/sk.json b/homeassistant/components/doorbird/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/doorbird/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index e4c7ac4e658ca..8bbf6197a1008 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -147,36 +147,6 @@ def async_get_options_flow(config_entry: ConfigEntry) -> DSMROptionFlowHandler: """Get the options flow for this handler.""" return DSMROptionFlowHandler(config_entry) - def _abort_if_host_port_configured( - self, - port: str, - host: str | None = None, - updates: dict[Any, Any] | None = None, - reload_on_update: bool = True, - ) -> FlowResult | None: - """Test if host and port are already configured.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port: - if updates is not None: - changed = self.hass.config_entries.async_update_entry( - entry, data={**entry.data, **updates} - ) - if ( - changed - and reload_on_update - and entry.state - in ( - config_entries.ConfigEntryState.LOADED, - config_entries.ConfigEntryState.SETUP_RETRY, - ) - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return self.async_abort(reason="already_configured") - - return None - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index d89c5a74db13e..e15a7c3b80a2b 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -5,5 +5,6 @@ "requirements": ["dsmr_parser==0.32"], "codeowners": ["@Robbie1221", "@frenck"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["dsmr_parser"] } diff --git a/homeassistant/components/dsmr/translations/cs.json b/homeassistant/components/dsmr/translations/cs.json index 8078da1b1a213..9ab3eefa6a689 100644 --- a/homeassistant/components/dsmr/translations/cs.json +++ b/homeassistant/components/dsmr/translations/cs.json @@ -4,6 +4,11 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "step": { + "setup_network": { + "data": { + "port": "Port" + } + }, "setup_serial": { "data": { "port": "Vyberte za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/dsmr/translations/el.json b/homeassistant/components/dsmr/translations/el.json new file mode 100644 index 0000000000000..77f2ad8910d9b --- /dev/null +++ b/homeassistant/components/dsmr/translations/el.json @@ -0,0 +1,53 @@ +{ + "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_communicate": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "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_communicate": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 DSMR", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "setup_serial": { + "data": { + "dsmr_version": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 DSMR", + "port": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "setup_serial_manual_path": { + "data": { + "port": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "title": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae" + }, + "user": { + "data": { + "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd [s]" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 DSMR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pt-BR.json b/homeassistant/components/dsmr/translations/pt-BR.json new file mode 100644 index 0000000000000..911a93db1b8c7 --- /dev/null +++ b/homeassistant/components/dsmr/translations/pt-BR.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_communicate": "Falha ao comunicar", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_communicate": "Falha ao comunicar", + "cannot_connect": "Falha ao conectar" + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "Selecione a vers\u00e3o do DSMR", + "host": "Nome do host", + "port": "Porta" + }, + "title": "Selecione o endere\u00e7o de conex\u00e3o" + }, + "setup_serial": { + "data": { + "dsmr_version": "Selecione a vers\u00e3o do DSMR", + "port": "Selecionar dispositivo" + }, + "title": "Dispositivo" + }, + "setup_serial_manual_path": { + "data": { + "port": "Caminho do Dispositivo USB" + }, + "title": "Caminho" + }, + "user": { + "data": { + "type": "Tipo de conex\u00e3o" + }, + "title": "Selecione o tipo de conex\u00e3o" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Tempo m\u00ednimo entre atualiza\u00e7\u00f5es de entidade [s]" + }, + "title": "Op\u00e7\u00f5es de DSMR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/sk.json b/homeassistant/components/dsmr/translations/sk.json new file mode 100644 index 0000000000000..e343d2e8b3188 --- /dev/null +++ b/homeassistant/components/dsmr/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "setup_network": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index bf5fd34788806..09d8090f4fc7e 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pdunehd==1.3.2"], "codeowners": ["@bieniu"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pdunehd"] } diff --git a/homeassistant/components/dunehd/translations/el.json b/homeassistant/components/dunehd/translations/el.json new file mode 100644 index 0000000000000..96ad16ac67f9a --- /dev/null +++ b/homeassistant/components/dunehd/translations/el.json @@ -0,0 +1,21 @@ +{ + "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": { + "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", + "invalid_host": "\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": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "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 Dune HD. \u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b7\u03b3\u03b1\u03af\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/dunehd \n\n\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7.", + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/pt-BR.json b/homeassistant/components/dunehd/translations/pt-BR.json new file mode 100644 index 0000000000000..072cf6011ea82 --- /dev/null +++ b/homeassistant/components/dunehd/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure a integra\u00e7\u00e3o Dune HD. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/dunehd \n\n Certifique-se de que seu player est\u00e1 ligado.", + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index 4fd54a7a3c9ba..8b4576f312ad7 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], "requirements": ["dwdwfsapi==1.0.5"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["dwdwfsapi"] } diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index 46edd2bacfa5f..078ea0ed21187 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": ["dweepy==0.3.0"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["dweepy"] } diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 1ae50233b1a3c..d403291a08144 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], "requirements": ["dynalite_devices==0.1.46"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["dynalite_devices_lib"] } diff --git a/homeassistant/components/eafm/manifest.json b/homeassistant/components/eafm/manifest.json index a4250e33a60b6..e3c1455b454d0 100644 --- a/homeassistant/components/eafm/manifest.json +++ b/homeassistant/components/eafm/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "codeowners": ["@Jc2k"], "requirements": ["aioeafm==0.1.2"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aioeafm"] } diff --git a/homeassistant/components/eafm/translations/el.json b/homeassistant/components/eafm/translations/el.json new file mode 100644 index 0000000000000..1854a65a7081e --- /dev/null +++ b/homeassistant/components/eafm/translations/el.json @@ -0,0 +1,17 @@ +{ + "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", + "no_stations": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bb\u03b7\u03bc\u03bc\u03c5\u03c1\u03ce\u03bd." + }, + "step": { + "user": { + "data": { + "station": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5", + "title": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bb\u03b7\u03bc\u03bc\u03c5\u03c1\u03ce\u03bd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/pt-BR.json b/homeassistant/components/eafm/translations/pt-BR.json new file mode 100644 index 0000000000000..f7dfd4cf08078 --- /dev/null +++ b/homeassistant/components/eafm/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_stations": "Nenhuma esta\u00e7\u00e3o de monitoramento de enchentes encontrada." + }, + "step": { + "user": { + "data": { + "station": "Esta\u00e7\u00e3o" + }, + "description": "Selecione a esta\u00e7\u00e3o que deseja monitorar", + "title": "Rastrear uma esta\u00e7\u00e3o de monitoramento de enchentes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 6e4aca44ad6af..3632b23123b8d 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": ["pyebox==1.1.4"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyebox"] } diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index ac5ca313f2f5a..ce631297db6e6 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -191,7 +191,7 @@ 4, SensorDeviceClass.TEMPERATURE, ], - "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4, None], + "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4, None], "AverageIgnitionTime": [ "averageIgnitiontime", TIME_SECONDS, diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 390e8efe7d5f4..fcb963f345d36 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": ["ebusdpy==0.0.17"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ebusdpy"] } diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index 83a9e7dbf6bdd..8c643555fe774 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": ["ecoaliface==0.4.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ecoaliface"] } diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index a22ec48da90c1..c9fe52b7e133d 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -16,5 +16,6 @@ {"type":"_sideplay._tcp.local.", "properties": {"mdl":"eb-*"}}, {"type":"_sideplay._tcp.local.", "properties": {"mdl":"ecobee*"}} ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyecobee"] } \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/el.json b/homeassistant/components/ecobee/translations/el.json index 341317af7fde2..a28aef55374af 100644 --- a/homeassistant/components/ecobee/translations/el.json +++ b/homeassistant/components/ecobee/translations/el.json @@ -1,14 +1,22 @@ { "config": { + "abort": { + "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": { "pin_request_failed": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 PIN \u03b1\u03c0\u03cc \u03c4\u03bf ecobee- \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc.", "token_request_failed": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 tokens \u03b1\u03c0\u03cc \u03c4\u03bf ecobee, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "step": { "authorize": { - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03bf https://www.ecobee.com/consumerportal/index.html \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN:\n\n{pin}\n\n\u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae." + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03bf https://www.ecobee.com/consumerportal/index.html \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN:\n\n{pin}\n\n\u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae.", + "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03bf ecobee.com" }, "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bb\u03ac\u03b2\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd ecobee.com.", "title": "\u03ba\u03bb\u03b5\u03b9\u03b4\u03af API ecobee" } } diff --git a/homeassistant/components/ecobee/translations/pt-BR.json b/homeassistant/components/ecobee/translations/pt-BR.json index 921319f55d027..3174c06c80274 100644 --- a/homeassistant/components/ecobee/translations/pt-BR.json +++ b/homeassistant/components/ecobee/translations/pt-BR.json @@ -1,16 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "error": { + "pin_request_failed": "Erro ao solicitar o PIN do ecobee; verifique se a chave da API est\u00e1 correta.", "token_request_failed": "Erro ao solicitar tokens da ecobee; Por favor, tente novamente." }, "step": { "authorize": { - "description": "Por favor, autorize este aplicativo em https://www.ecobee.com/consumerportal/index.html com c\u00f3digo PIN:\n\n{pin}\n\nEm seguida, pressione Submit.", + "description": "Por favor, autorize este aplicativo em https://www.ecobee.com/consumerportal/index.html com c\u00f3digo PIN:\n\n{pin}\n\nEm seguida, pressione Enviar.", "title": "Autorizar aplicativo em ecobee.com" }, "user": { "data": { - "api_key": "Chave API" + "api_key": "Chave da API" }, "description": "Por favor, insira a chave de API obtida em ecobee.com.", "title": "chave da API ecobee" diff --git a/homeassistant/components/ecobee/translations/sk.json b/homeassistant/components/ecobee/translations/sk.json new file mode 100644 index 0000000000000..9d5ee388dc325 --- /dev/null +++ b/homeassistant/components/ecobee/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/uk.json b/homeassistant/components/ecobee/translations/uk.json index 7cf7df534296f..0d411f1fdd105 100644 --- a/homeassistant/components/ecobee/translations/uk.json +++ b/homeassistant/components/ecobee/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "pin_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 PIN-\u043a\u043e\u0434\u0443 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", diff --git a/homeassistant/components/ecobee/translations/zh-Hant.json b/homeassistant/components/ecobee/translations/zh-Hant.json index b46042182063a..21e769013baf7 100644 --- a/homeassistant/components/ecobee/translations/zh-Hant.json +++ b/homeassistant/components/ecobee/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u91d1\u9470\u6b63\u78ba\u6027\u3002", diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 99a021de73a39..f8df1a4134ec4 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,8 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.14"], + "requirements": ["pyeconet==0.1.15"], "codeowners": ["@vangorra", "@w1ll1am23"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["paho_mqtt", "pyeconet"] } diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 9dbe46ab989b7..e39f55423d42a 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -13,9 +13,9 @@ ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu" TANK_HEALTH = "tank_health" -AVAILIBLE_HOT_WATER = "availible_hot_water" +AVAILABLE_HOT_WATER = "available_hot_water" COMPRESSOR_HEALTH = "compressor_health" -OVERRIDE_STATUS = "oveerride_status" +OVERRIDE_STATUS = "override_status" WATER_USAGE_TODAY = "water_usage_today" POWER_USAGE_TODAY = "power_usage_today" ALERT_COUNT = "alert_count" @@ -24,7 +24,7 @@ SENSOR_NAMES_TO_ATTRIBUTES = { TANK_HEALTH: "tank_health", - AVAILIBLE_HOT_WATER: "tank_hot_water_availability", + AVAILABLE_HOT_WATER: "tank_hot_water_availability", COMPRESSOR_HEALTH: "compressor_health", OVERRIDE_STATUS: "override_status", WATER_USAGE_TODAY: "todays_water_usage", @@ -36,7 +36,7 @@ SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = { TANK_HEALTH: PERCENTAGE, - AVAILIBLE_HOT_WATER: PERCENTAGE, + AVAILABLE_HOT_WATER: PERCENTAGE, COMPRESSOR_HEALTH: PERCENTAGE, OVERRIDE_STATUS: None, WATER_USAGE_TODAY: VOLUME_GALLONS, diff --git a/homeassistant/components/econet/translations/el.json b/homeassistant/components/econet/translations/el.json new file mode 100644 index 0000000000000..b68034484d35c --- /dev/null +++ b/homeassistant/components/econet/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", + "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" + }, + "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" + }, + "step": { + "user": { + "data": { + "email": "Email", + "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 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/pt-BR.json b/homeassistant/components/econet/translations/pt-BR.json new file mode 100644 index 0000000000000..23469f0fd263f --- /dev/null +++ b/homeassistant/components/econet/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "title": "Configurar conta Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/sk.json b/homeassistant/components/econet/translations/sk.json new file mode 100644 index 0000000000000..1a3e5d67caad3 --- /dev/null +++ b/homeassistant/components/econet/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index ad442b0621a72..1712cea1578e5 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": ["sucks==0.9.4"], "codeowners": ["@OverloadUT"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["sleekxmppfs", "sucks"] } diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index 92ab636b87f9f..64ec4bca3a721 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["beacontools"] } diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 6226968b5d313..da89298c8739c 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": ["pyedimax==0.2.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyedimax"] } diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index 7505f5e243886..4cffabe87fc39 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/edl21", "requirements": ["pysml==0.0.7"], "codeowners": ["@mtdcr"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["sml"] } diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index f52cc0c17d46e..278ac004121cf 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -15,7 +15,14 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONF_NAME, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, + POWER_WATT, +) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -238,6 +245,14 @@ SENSORS = {desc.key: desc for desc in SENSOR_TYPES} +SENSOR_UNIT_MAPPING = { + "Wh": ENERGY_WATT_HOUR, + "kWh": ENERGY_KILO_WATT_HOUR, + "W": POWER_WATT, + "A": ELECTRIC_CURRENT_AMPERE, + "V": ELECTRIC_POTENTIAL_VOLT, +} + async def async_setup_platform( hass: HomeAssistant, @@ -435,4 +450,7 @@ def extra_state_attributes(self): @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - return self._telegram.get("unit") + if (unit := self._telegram.get("unit")) is None: + return None + + return SENSOR_UNIT_MAPPING[unit] diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index 966df3ed85875..fc90591cae639 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -3,7 +3,8 @@ "name": "Efergy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.1.5"], + "requirements": ["pyefergy==22.1.1"], "codeowners": ["@tkdrob"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["iso4217", "pyefergy"] } diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 21d4002bcdb02..00a10b713d28f 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -3,8 +3,10 @@ import logging from re import sub +from typing import cast -from pyefergy import Efergy, exceptions +from pyefergy import Efergy +from pyefergy.exceptions import ConnectError, DataError, ServiceError from homeassistant.components.sensor import ( SensorDeviceClass, @@ -16,6 +18,7 @@ from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform +from homeassistant.helpers.typing import StateType from . import EfergyEntity from .const import CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN @@ -123,8 +126,8 @@ async def async_setup_entry( ) ) else: - description.entity_registry_enabled_default = len(api.info["sids"]) > 1 - for sid in api.info["sids"]: + description.entity_registry_enabled_default = len(api.sids) > 1 + for sid in api.sids: sensors.append( EfergySensor( api, @@ -146,14 +149,16 @@ def __init__( server_unique_id: str, period: str | None = None, currency: str | None = None, - sid: str = "", + sid: int | None = None, ) -> None: """Initialize the sensor.""" super().__init__(api, server_unique_id) self.entity_description = description if description.key == CONF_CURRENT_VALUES: - self._attr_name = f"{description.name}_{sid}" - self._attr_unique_id = f"{server_unique_id}/{description.key}_{sid}" + self._attr_name = f"{description.name}_{'' if sid is None else sid}" + self._attr_unique_id = ( + f"{server_unique_id}/{description.key}_{'' if sid is None else sid}" + ) if "cost" in description.key: self._attr_native_unit_of_measurement = currency self.sid = sid @@ -162,10 +167,11 @@ def __init__( async def async_update(self) -> None: """Get the Efergy monitor data from the web service.""" try: - self._attr_native_value = await self.api.async_get_reading( + data = await self.api.async_get_reading( self.entity_description.key, period=self.period, sid=self.sid ) - except (exceptions.DataError, exceptions.ConnectError) as ex: + self._attr_native_value = cast(StateType, data) + except (ConnectError, DataError, ServiceError) as ex: if self._attr_available: self._attr_available = False _LOGGER.error("Error getting data: %s", ex) diff --git a/homeassistant/components/efergy/translations/el.json b/homeassistant/components/efergy/translations/el.json new file mode 100644 index 0000000000000..f1206afd71baf --- /dev/null +++ b/homeassistant/components/efergy/translations/el.json @@ -0,0 +1,21 @@ +{ + "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", + "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", + "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": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/pt-BR.json b/homeassistant/components/efergy/translations/pt-BR.json new file mode 100644 index 0000000000000..8197121b5d57b --- /dev/null +++ b/homeassistant/components/efergy/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/sk.json b/homeassistant/components/efergy/translations/sk.json new file mode 100644 index 0000000000000..64731388e98c0 --- /dev/null +++ b/homeassistant/components/efergy/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 78e32a4d74965..7ea598e266cfa 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/egardia", "requirements": ["pythonegardia==1.0.40"], "codeowners": ["@jeroenterheerdt"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pythonegardia"] } diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 06af3defac3ac..e4c5a1e002952 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": ["pyeight==0.2.0"], "codeowners": ["@mezz64", "@raman325"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyeight"] } diff --git a/homeassistant/components/elgato/translations/el.json b/homeassistant/components/elgato/translations/el.json index 1f15d6d6eb716..5f6b487c58d50 100644 --- a/homeassistant/components/elgato/translations/el.json +++ b/homeassistant/components/elgato/translations/el.json @@ -1,6 +1,7 @@ { "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": { @@ -9,10 +10,15 @@ "flow_title": "{serial_number}", "step": { "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Elgato Light \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." }, "zeroconf_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Elgato Light \u03bc\u03b5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc `{serial_number}` \u03c3\u03c4\u03bf Home Assistant;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Elgato Light \u03bc\u03b5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc `{serial_number}` \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Elgato Light" } } } diff --git a/homeassistant/components/elgato/translations/pt-BR.json b/homeassistant/components/elgato/translations/pt-BR.json index 02edb70761896..4cc692371d3b5 100644 --- a/homeassistant/components/elgato/translations/pt-BR.json +++ b/homeassistant/components/elgato/translations/pt-BR.json @@ -1,14 +1,24 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{serial_number}", "step": { "user": { "data": { + "host": "Nome do host", "port": "Porta" - } + }, + "description": "Configure seu Elgato Light para integrar com o Home Assistant." }, "zeroconf_confirm": { - "description": "Deseja adicionar o Elgato Key Light n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", - "title": "Dispositivo Elgato Key Light descoberto" + "description": "Deseja adicionar a l\u00e2mpada Elgato com n\u00famero de s\u00e9rie `{serial_number}` ao Home Assistant?", + "title": "Dispositivo Elgato Key descoberto" } } } diff --git a/homeassistant/components/elgato/translations/sk.json b/homeassistant/components/elgato/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/elgato/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 6f5864235526b..04a26f2822b06 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -6,6 +6,7 @@ import re from types import MappingProxyType from typing import Any +from urllib.parse import urlparse import async_timeout import elkm1_lib as elkm1 @@ -13,6 +14,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_CONNECTIONS, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, @@ -27,16 +29,17 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from homeassistant.util.network import is_ip_address from .const import ( ATTR_KEY, ATTR_KEY_NAME, ATTR_KEYPAD_ID, - BARE_TEMP_CELSIUS, - BARE_TEMP_FAHRENHEIT, CONF_AREA, CONF_AUTO_CONFIGURE, CONF_COUNTER, @@ -48,9 +51,18 @@ CONF_TASK, CONF_THERMOSTAT, CONF_ZONE, + DISCOVER_SCAN_TIMEOUT, + DISCOVERY_INTERVAL, DOMAIN, ELK_ELEMENTS, EVENT_ELKM1_KEYPAD_KEY_PRESSED, + LOGIN_TIMEOUT, +) +from .discovery import ( + async_discover_device, + async_discover_devices, + async_trigger_discovery, + async_update_entry_from_discovery, ) SYNC_TIMEOUT = 120 @@ -127,28 +139,28 @@ def _has_all_unique_prefixes(value): } ) -DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PREFIX, default=""): vol.All(cv.string, vol.Lower), - vol.Optional(CONF_USERNAME, default=""): cv.string, - vol.Optional(CONF_PASSWORD, default=""): cv.string, - vol.Optional(CONF_AUTO_CONFIGURE, default=False): cv.boolean, - # cv.temperature_unit will mutate 'C' -> '°C' and 'F' -> '°F' - vol.Optional( - CONF_TEMPERATURE_UNIT, default=BARE_TEMP_FAHRENHEIT - ): cv.temperature_unit, - vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_KEYPAD, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_OUTPUT, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_PLC, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_SETTING, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_TASK, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN, - }, - _host_validator, +DEVICE_SCHEMA = vol.All( + cv.deprecated(CONF_TEMPERATURE_UNIT), + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PREFIX, default=""): vol.All(cv.string, vol.Lower), + vol.Optional(CONF_USERNAME, default=""): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, + vol.Optional(CONF_AUTO_CONFIGURE, default=False): cv.boolean, + vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): cv.temperature_unit, + vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_KEYPAD, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_OUTPUT, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_PLC, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_SETTING, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_TASK, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN, + vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN, + }, + _host_validator, + ), ) CONFIG_SCHEMA = vol.Schema( @@ -162,6 +174,14 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: hass.data.setdefault(DOMAIN, {}) _create_elk_services(hass) + async def _async_discovery(*_: Any) -> None: + async_trigger_discovery( + hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT) + ) + + asyncio.create_task(_async_discovery()) + async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL) + if DOMAIN not in hass_config: return True @@ -204,13 +224,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elk-M1 Control from a config entry.""" conf: MappingProxyType[str, Any] = entry.data + host = urlparse(entry.data[CONF_HOST]).hostname + _LOGGER.debug("Setting up elkm1 %s", conf["host"]) - temperature_unit = TEMP_FAHRENHEIT - if conf[CONF_TEMPERATURE_UNIT] in (BARE_TEMP_CELSIUS, TEMP_CELSIUS): - temperature_unit = TEMP_CELSIUS + if (not entry.unique_id or ":" not in entry.unique_id) and is_ip_address(host): + _LOGGER.debug( + "Unique id for %s is missing during setup, trying to fill from discovery", + host, + ) + if device := await async_discover_device(hass, host): + async_update_entry_from_discovery(hass, entry, device) - config: dict[str, Any] = {"temperature_unit": temperature_unit} + config: dict[str, Any] = {} if not conf[CONF_AUTO_CONFIGURE]: # With elkm1-lib==0.7.16 and later auto configure is available @@ -253,14 +279,20 @@ def _element_changed(element, changeset): keypad.add_callback(_element_changed) try: - if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT, conf[CONF_HOST]): + if not await async_wait_for_elk_to_sync( + elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) + ): return False except asyncio.TimeoutError as exc: - raise ConfigEntryNotReady from exc + raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc + elk_temp_unit = elk.panel.temperature_units # pylint: disable=no-member + temperature_unit = TEMP_CELSIUS if elk_temp_unit == "C" else TEMP_FAHRENHEIT + config["temperature_unit"] = temperature_unit hass.data[DOMAIN][entry.entry_id] = { "elk": elk, "prefix": conf[CONF_PREFIX], + "mac": entry.unique_id, "auto_configure": conf[CONF_AUTO_CONFIGURE], "config": config, "keypads": {}, @@ -298,38 +330,51 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_wait_for_elk_to_sync(elk, timeout, conf_host): +async def async_wait_for_elk_to_sync( + elk: elkm1.Elk, + login_timeout: int, + sync_timeout: int, + password_auth: bool, +) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" + sync_event = asyncio.Event() + login_event = asyncio.Event() + def login_status(succeeded): nonlocal success success = succeeded if succeeded: _LOGGER.debug("ElkM1 login succeeded") + login_event.set() else: elk.disconnect() _LOGGER.error("ElkM1 login failed; invalid username or password") - event.set() + login_event.set() + sync_event.set() def sync_complete(): - event.set() + sync_event.set() success = True - event = asyncio.Event() elk.add_handler("login", login_status) elk.add_handler("sync_complete", sync_complete) - try: - async with async_timeout.timeout(timeout): - await event.wait() - except asyncio.TimeoutError: - _LOGGER.error( - "Timed out after %d seconds while trying to sync with ElkM1 at %s", - timeout, - conf_host, - ) - elk.disconnect() - raise + events = [] + if password_auth: + events.append(("login", login_event, login_timeout)) + events.append(("sync_complete", sync_event, sync_timeout)) + + for name, event, timeout in events: + _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) + try: + async with async_timeout.timeout(timeout): + await event.wait() + except asyncio.TimeoutError: + _LOGGER.debug("Timed out waiting for %s event", name) + elk.disconnect() + raise + _LOGGER.debug("Received %s event", name) return success @@ -391,7 +436,9 @@ def __init__(self, element, elk, elk_data): """Initialize the base of all Elk devices.""" self._elk = elk self._element = element + self._mac = elk_data["mac"] self._prefix = elk_data["prefix"] + self._name_prefix = f"{self._prefix} " if self._prefix else "" self._temperature_unit = elk_data["config"]["temperature_unit"] # unique_id starts with elkm1_ iff there is no prefix # it starts with elkm1m_{prefix} iff there is a prefix @@ -410,7 +457,7 @@ def __init__(self, element, elk, elk_data): @property def name(self): """Name of the element.""" - return f"{self._prefix}{self._element.name}" + return f"{self._name_prefix}{self._element.name}" @property def unique_id(self): @@ -469,10 +516,13 @@ def device_info(self) -> DeviceInfo: device_name = "ElkM1" if self._prefix: device_name += f" {self._prefix}" - return DeviceInfo( + device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._prefix}_system")}, manufacturer="ELK Products, Inc.", model="M1", name=device_name, sw_version=self._elk.panel.elkm1_version, ) + if self._mac: + device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, self._mac)} + return device_info diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 905aa35ad1900..a21cf186005c5 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -1,27 +1,43 @@ """Config flow for Elk-M1 Control integration.""" +from __future__ import annotations + import asyncio import logging +from typing import Any from urllib.parse import urlparse import elkm1_lib as elkm1 +from elkm1_lib.discovery import ElkSystem import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components import dhcp from homeassistant.const import ( CONF_ADDRESS, CONF_HOST, CONF_PASSWORD, CONF_PREFIX, CONF_PROTOCOL, - CONF_TEMPERATURE_UNIT, CONF_USERNAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify +from homeassistant.util.network import is_ip_address from . import async_wait_for_elk_to_sync -from .const import CONF_AUTO_CONFIGURE, DOMAIN +from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT +from .discovery import ( + _short_mac, + async_discover_device, + async_discover_devices, + async_update_entry_from_discovery, +) + +CONF_DEVICE = "device" + +SECURE_PORT = 2601 _LOGGER = logging.getLogger(__name__) @@ -32,25 +48,20 @@ "serial": "serial://", } -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_PROTOCOL, default="secure"): vol.In( - ["secure", "TLS 1.2", "non-secure", "serial"] - ), - vol.Required(CONF_ADDRESS): str, - vol.Optional(CONF_USERNAME, default=""): str, - vol.Optional(CONF_PASSWORD, default=""): str, - vol.Optional(CONF_PREFIX, default=""): str, - vol.Optional(CONF_TEMPERATURE_UNIT, default=TEMP_FAHRENHEIT): vol.In( - [TEMP_FAHRENHEIT, TEMP_CELSIUS] - ), - } -) - VALIDATE_TIMEOUT = 35 +BASE_SCHEMA = { + vol.Optional(CONF_USERNAME, default=""): str, + vol.Optional(CONF_PASSWORD, default=""): str, +} + +SECURE_PROTOCOLS = ["secure", "TLS 1.2"] +ALL_PROTOCOLS = [*SECURE_PROTOCOLS, "non-secure", "serial"] +DEFAULT_SECURE_PROTOCOL = "secure" +DEFAULT_NON_SECURE_PROTOCOL = "non-secure" + -async def validate_input(data): +async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -70,11 +81,18 @@ async def validate_input(data): ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT, url): + if not await async_wait_for_elk_to_sync( + elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) + ): raise InvalidAuth - device_name = data[CONF_PREFIX] if data[CONF_PREFIX] else "ElkM1" - # Return info that you want to store in the config entry. + short_mac = _short_mac(mac) if mac else None + if prefix and prefix != short_mac: + device_name = prefix + elif mac: + device_name = f"ElkM1 {short_mac}" + else: + device_name = "ElkM1" return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)} @@ -87,6 +105,13 @@ def _make_url_from_data(data): return f"{protocol}{address}" +def _placeholders_from_device(device: ElkSystem) -> dict[str, str]: + return { + "mac_address": _short_mac(device.mac_address), + "host": f"{device.ip_address}:{device.port}", + } + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Elk-M1 Control.""" @@ -94,53 +119,215 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the elkm1 config flow.""" - self.importing = False + self._discovered_device: ElkSystem | None = None + self._discovered_devices: dict[str, ElkSystem] = {} + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + self._discovered_device = ElkSystem( + discovery_info.macaddress, discovery_info.ip, 0 + ) + _LOGGER.debug("Elk discovered from dhcp: %s", self._discovered_device) + return await self._async_handle_discovery() + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle integration discovery.""" + self._discovered_device = ElkSystem( + discovery_info["mac_address"], + discovery_info["ip_address"], + discovery_info["port"], + ) + _LOGGER.debug( + "Elk discovered from integration discovery: %s", self._discovered_device + ) + return await self._async_handle_discovery() + + async def _async_handle_discovery(self) -> FlowResult: + """Handle any discovery.""" + device = self._discovered_device + assert device is not None + mac = dr.format_mac(device.mac_address) + host = device.ip_address + await self.async_set_unique_id(mac) + for entry in self._async_current_entries(include_ignore=False): + if ( + entry.unique_id == mac + or urlparse(entry.data[CONF_HOST]).hostname == host + ): + if async_update_entry_from_discovery(self.hass, entry, device): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + self.context[CONF_HOST] = host + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + return self.async_abort(reason="already_in_progress") + if not device.port: + if discovered_device := await async_discover_device(self.hass, host): + self._discovered_device = discovered_device + else: + return self.async_abort(reason="cannot_connect") + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + self.context["title_placeholders"] = _placeholders_from_device( + self._discovered_device + ) + return await self.async_step_discovered_connection() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" + if user_input is not None: + if mac := user_input[CONF_DEVICE]: + await self.async_set_unique_id(mac, raise_on_progress=False) + self._discovered_device = self._discovered_devices[mac] + return await self.async_step_discovered_connection() + return await self.async_step_manual_connection() + + current_unique_ids = self._async_current_ids() + current_hosts = { + urlparse(entry.data[CONF_HOST]).hostname + for entry in self._async_current_entries(include_ignore=False) + } + discovered_devices = await async_discover_devices( + self.hass, DISCOVER_SCAN_TIMEOUT + ) + self._discovered_devices = { + dr.format_mac(device.mac_address): device for device in discovered_devices + } + devices_name: dict[str | None, str] = { + mac: f"{_short_mac(device.mac_address)} ({device.ip_address})" + for mac, device in self._discovered_devices.items() + if mac not in current_unique_ids and device.ip_address not in current_hosts + } + if not devices_name: + return await self.async_step_manual_connection() + devices_name[None] = "Manual Entry" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def _async_create_or_error( + self, user_input: dict[str, Any], importing: bool + ) -> tuple[dict[str, str] | None, FlowResult | None]: + """Try to connect and create the entry or error.""" + if self._url_already_configured(_make_url_from_data(user_input)): + return None, self.async_abort(reason="address_already_configured") + + try: + info = await validate_input(user_input, self.unique_id) + except asyncio.TimeoutError: + return {CONF_HOST: "cannot_connect"}, None + except InvalidAuth: + return {CONF_PASSWORD: "invalid_auth"}, None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return {"base": "unknown"}, None + + if importing: + return None, self.async_create_entry(title=info["title"], data=user_input) + + return None, self.async_create_entry( + title=info["title"], + data={ + CONF_HOST: info[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_AUTO_CONFIGURE: True, + CONF_PREFIX: info[CONF_PREFIX], + }, + ) + + async def async_step_discovered_connection(self, user_input=None): + """Handle connecting the device when we have a discovery.""" errors = {} + device = self._discovered_device + assert device is not None if user_input is not None: - if self._url_already_configured(_make_url_from_data(user_input)): - return self.async_abort(reason="address_already_configured") - - try: - info = await validate_input(user_input) - - except asyncio.TimeoutError: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if "base" not in errors: - await self.async_set_unique_id(user_input[CONF_PREFIX]) - self._abort_if_unique_id_configured() + user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}" + if self._async_current_entries(): + user_input[CONF_PREFIX] = _short_mac(device.mac_address) + else: + user_input[CONF_PREFIX] = "" + if device.port != SECURE_PORT: + user_input[CONF_PROTOCOL] = DEFAULT_NON_SECURE_PROTOCOL + errors, result = await self._async_create_or_error(user_input, False) + if not errors: + return result + + base_schmea = BASE_SCHEMA.copy() + if device.port == SECURE_PORT: + base_schmea[ + vol.Required(CONF_PROTOCOL, default=DEFAULT_SECURE_PROTOCOL) + ] = vol.In(SECURE_PROTOCOLS) - if self.importing: - return self.async_create_entry(title=info["title"], data=user_input) - - return self.async_create_entry( - title=info["title"], - data={ - CONF_HOST: info[CONF_HOST], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_AUTO_CONFIGURE: True, - CONF_TEMPERATURE_UNIT: user_input[CONF_TEMPERATURE_UNIT], - CONF_PREFIX: info[CONF_PREFIX], - }, - ) + return self.async_show_form( + step_id="discovered_connection", + data_schema=vol.Schema(base_schmea), + errors=errors, + description_placeholders=_placeholders_from_device(device), + ) + + async def async_step_manual_connection(self, user_input=None): + """Handle connecting the device when we need manual entry.""" + errors = {} + if user_input is not None: + # We might be able to discover the device via directed UDP + # in case its on another subnet + if device := await async_discover_device( + self.hass, user_input[CONF_ADDRESS] + ): + await self.async_set_unique_id(dr.format_mac(device.mac_address)) + self._abort_if_unique_id_configured() + user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}" + errors, result = await self._async_create_or_error(user_input, False) + if not errors: + return result return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="manual_connection", + data_schema=vol.Schema( + { + **BASE_SCHEMA, + vol.Required(CONF_ADDRESS): str, + vol.Optional(CONF_PREFIX, default=""): str, + vol.Required( + CONF_PROTOCOL, default=DEFAULT_SECURE_PROTOCOL + ): vol.In(ALL_PROTOCOLS), + } + ), + errors=errors, ) async def async_step_import(self, user_input): """Handle import.""" - self.importing = True - return await self.async_step_user(user_input) + _LOGGER.debug("Elk is importing from yaml") + url = _make_url_from_data(user_input) + + if self._url_already_configured(url): + return self.async_abort(reason="address_already_configured") + + host = urlparse(url).hostname + _LOGGER.debug( + "Importing is trying to fill unique id from discovery for %s", host + ) + if is_ip_address(host) and ( + device := await async_discover_device(self.hass, host) + ): + await self.async_set_unique_id(dr.format_mac(device.mac_address)) + self._abort_if_unique_id_configured() + + return (await self._async_create_or_error(user_input, True))[1] def _url_already_configured(self, url): """See if we already have a elkm1 matching user input configured.""" diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 4d2dac4b1de1c..fd4856bd5d5d2 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -1,5 +1,7 @@ """Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels.""" +from datetime import timedelta + from elkm1_lib.const import Max import voluptuous as vol @@ -7,6 +9,8 @@ DOMAIN = "elkm1" +LOGIN_TIMEOUT = 20 + CONF_AUTO_CONFIGURE = "auto_configure" CONF_AREA = "area" CONF_COUNTER = "counter" @@ -18,9 +22,8 @@ CONF_TASK = "task" CONF_THERMOSTAT = "thermostat" - -BARE_TEMP_FAHRENHEIT = "F" -BARE_TEMP_CELSIUS = "C" +DISCOVER_SCAN_TIMEOUT = 10 +DISCOVERY_INTERVAL = timedelta(minutes=15) ELK_ELEMENTS = { CONF_AREA: Max.AREAS.value, diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py new file mode 100644 index 0000000000000..326698c36869d --- /dev/null +++ b/homeassistant/components/elkm1/discovery.py @@ -0,0 +1,96 @@ +"""The elkm1 integration discovery.""" +from __future__ import annotations + +import asyncio +from dataclasses import asdict +import logging + +from elkm1_lib.discovery import AIOELKDiscovery, ElkSystem + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr + +from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def _short_mac(mac_address: str) -> str: + return mac_address.replace(":", "")[-6:] + + +@callback +def async_update_entry_from_discovery( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + device: ElkSystem, +) -> bool: + """Update a config entry from a discovery.""" + if not entry.unique_id or ":" not in entry.unique_id: + _LOGGER.debug("Adding unique id from discovery: %s", device) + return hass.config_entries.async_update_entry( + entry, unique_id=dr.format_mac(device.mac_address) + ) + _LOGGER.debug("Unique id is already present from discovery: %s", device) + return False + + +async def async_discover_devices( + hass: HomeAssistant, timeout: int, address: str | None = None +) -> list[ElkSystem]: + """Discover elkm1 devices.""" + if address: + targets = [address] + else: + targets = [ + str(address) + for address in await network.async_get_ipv4_broadcast_addresses(hass) + ] + + scanner = AIOELKDiscovery() + combined_discoveries: dict[str, ElkSystem] = {} + for idx, discovered in enumerate( + await asyncio.gather( + *[ + scanner.async_scan(timeout=timeout, address=address) + for address in targets + ], + return_exceptions=True, + ) + ): + if isinstance(discovered, Exception): + _LOGGER.debug("Scanning %s failed with error: %s", targets[idx], discovered) + continue + for device in discovered: + assert isinstance(device, ElkSystem) + combined_discoveries[device.ip_address] = device + + return list(combined_discoveries.values()) + + +async def async_discover_device(hass: HomeAssistant, host: str) -> ElkSystem | None: + """Direct discovery at a single ip instead of broadcast.""" + # If we are missing the unique_id we should be able to fetch it + # from the device by doing a directed discovery at the host only + for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): + if device.ip_address == host: + return device + return None + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: list[ElkSystem], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), + ) + ) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 3b341d90669cd..909bfa3bd02f7 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,8 +2,14 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==1.0.0"], + "requirements": ["elkm1-lib==1.2.0"], + "dhcp": [ + {"registered_devices": true}, + {"macaddress":"00409D*"} + ], "codeowners": ["@gwww", "@bdraco"], + "dependencies": ["network"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["elkm1_lib"] } diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index bf0da956d445c..35672d5df80ee 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -1,8 +1,16 @@ { "config": { + "flow_title": "{mac_address} ({host})", "step": { "user": { "title": "Connect to Elk-M1 Control", + "description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.", + "data": { + "device": "Device" + } + }, + "manual_connection": { + "title": "[%key:component::elkm1::config::step::user::title%]", "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", "data": { "protocol": "Protocol", @@ -12,6 +20,16 @@ "prefix": "A unique prefix (leave blank if you only have one ElkM1).", "temperature_unit": "The temperature unit ElkM1 uses." } + }, + "discovered_connection": { + "title": "[%key:component::elkm1::config::step::user::title%]", + "description": "Connect to the discovered system: {mac_address} ({host})", + "data": { + "protocol": "[%key:component::elkm1::config::step::manual_connection::data::protocol%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "temperature_unit": "[%key:component::elkm1::config::step::manual_connection::data::temperature_unit%]" + } } }, "error": { @@ -20,8 +38,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "An ElkM1 with this prefix is already configured", "address_already_configured": "An ElkM1 with this address is already configured" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/elkm1/translations/bg.json b/homeassistant/components/elkm1/translations/bg.json new file mode 100644 index 0000000000000..8fd587a064038 --- /dev/null +++ b/homeassistant/components/elkm1/translations/bg.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{mac_address} ({host})", + "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "manual_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/ca.json b/homeassistant/components/elkm1/translations/ca.json index ce766c314ed9a..2317e475a374a 100644 --- a/homeassistant/components/elkm1/translations/ca.json +++ b/homeassistant/components/elkm1/translations/ca.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "Ja hi ha un Elk-M1 configurat amb aquesta adre\u00e7a", - "already_configured": "Ja hi ha un Elk-M1 configurat amb aquest prefix" + "already_configured": "Ja hi ha un Elk-M1 configurat amb aquest prefix", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Contrasenya", + "protocol": "Protocol", + "temperature_unit": "Unitat de temperatura que utilitza ElkM1.", + "username": "Nom d'usuari" + }, + "description": "Connecta't al sistema descobert: {mac_address} ({host})", + "title": "Connexi\u00f3 amb el controlador Elk-M1" + }, + "manual_connection": { + "data": { + "address": "Adre\u00e7a IP, domini o port s\u00e8rie (en cas d'una connexi\u00f3 s\u00e8rie).", + "password": "Contrasenya", + "prefix": "Prefix \u00fanic (deixa-ho en blanc si nom\u00e9s tens un \u00fanic controlador ElkM1).", + "protocol": "Protocol", + "temperature_unit": "Unitat de temperatura que utilitza ElkM1.", + "username": "Nom d'usuari" + }, + "description": "La cadena de car\u00e0cters (string) de l'adre\u00e7a ha de tenir el format: 'adre\u00e7a[:port]' tant per al mode 'segur' com el 'no segur'. Exemple: '192.168.1.1'. El port \u00e9s opcional, per defecte \u00e9s el 2101 pel mode 'no segur' i el 2601 pel 'segur'. Per al protocol s\u00e8rie, l'adre\u00e7a ha de tenir el format 'tty[:baud]'. Exemple: '/dev/ttyS1'. La velocitat en bauds \u00e9s opcional (115200 per defecte).", + "title": "Connexi\u00f3 amb el controlador Elk-M1" + }, "user": { "data": { - "address": "Adre\u00e7a IP, domini o port s\u00e8rie (si es est\u00e0 connectat amb una connexi\u00f3 s\u00e8rie).", + "address": "Adre\u00e7a IP, domini o port s\u00e8rie (en cas d'una connexi\u00f3 s\u00e8rie).", + "device": "Dispositiu", "password": "Contrasenya", "prefix": "Prefix \u00fanic (deixa-ho en blanc si nom\u00e9s tens un \u00fanic controlador Elk-M1).", "protocol": "Protocol", "temperature_unit": "Unitats de temperatura que utilitza l'Elk-M1.", "username": "Nom d'usuari" }, - "description": "La cadena de car\u00e0cters (string) de l'adre\u00e7a ha de tenir el format: 'adre\u00e7a[:port]' tant per al mode 'segur' com el 'no segur'. Exemple: '192.168.1.1'. El port \u00e9s opcional, per defecte \u00e9s el 2101 pel mode 'no segur' i el 2601 pel 'segur'. Per al protocol s\u00e8rie, l'adre\u00e7a ha de tenir el format 'tty[:baud]'. Exemple: '/dev/ttyS1'. La velocitat en bauds \u00e9s opcional (115200 per defecte).", + "description": "Selecciona un sistema descobert o 'entrada manual' si no s'han descobert dispositius.", "title": "Connexi\u00f3 amb el controlador Elk-M1" } } diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json index 2b84b802b6b50..f2f4c17af9ccd 100644 --- a/homeassistant/components/elkm1/translations/cs.json +++ b/homeassistant/components/elkm1/translations/cs.json @@ -2,14 +2,31 @@ "config": { "abort": { "address_already_configured": "ElkM1 s touto adresou je ji\u017e nastaven", - "already_configured": "ElkM1 s t\u00edmto prefixem je ji\u017e nastaven" + "already_configured": "ElkM1 s t\u00edmto prefixem je ji\u017e nastaven", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "P\u0159ipojen\u00ed k ovlada\u010di Elk-M1" + }, + "manual_connection": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "P\u0159ipojen\u00ed k ovlada\u010di Elk-M1" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index 137f781fd05a8..cf318a626c9ff 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "Ein ElkM1 mit dieser Adresse ist bereits konfiguriert", - "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert" + "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbinden fehlgeschlagen" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "Passwort", + "protocol": "Protokoll", + "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", + "username": "Benutzername" + }, + "description": "Verbinde dich mit dem ermittelten System: {mac_address} ( {host} )", + "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" + }, + "manual_connection": { "data": { "address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.", "password": "Passwort", @@ -21,6 +34,19 @@ }, "description": "Die Adresszeichenfolge muss in der Form 'adresse[:port]' f\u00fcr 'sicher' und 'nicht sicher' vorliegen. Beispiel: '192.168.1.1'. Der Port ist optional und standardm\u00e4\u00dfig 2101 f\u00fcr \"nicht sicher\" und 2601 f\u00fcr \"sicher\". F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Beispiel: '/dev/ttyS1'. Der Baudrate ist optional und standardm\u00e4\u00dfig 115200.", "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" + }, + "user": { + "data": { + "address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.", + "device": "Ger\u00e4t", + "password": "Passwort", + "prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn du nur einen ElkM1 hast).", + "protocol": "Protokoll", + "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", + "username": "Benutzername" + }, + "description": "W\u00e4hle ein erkanntes System oder \"Manuelle Eingabe\", wenn keine Ger\u00e4te erkannt wurden.", + "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" } } } diff --git a/homeassistant/components/elkm1/translations/el.json b/homeassistant/components/elkm1/translations/el.json index 4ae9a2d228af9..9e7fe27ce7260 100644 --- a/homeassistant/components/elkm1/translations/el.json +++ b/homeassistant/components/elkm1/translations/el.json @@ -1,7 +1,50 @@ { "config": { + "abort": { + "address_already_configured": "\u0388\u03bd\u03b1 ElkM1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_configured": "\u0388\u03bd\u03b1 ElkM1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \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", + "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", + "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" + }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf", + "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5: {mac_address} ({host})", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" + }, + "manual_connection": { + "data": { + "address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bf \u03c4\u03bf\u03bc\u03ad\u03b1\u03c2 \u03ae \u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b5\u03ac\u03bd \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "prefix": "\u0388\u03bd\u03b1 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b1\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 ElkM1).", + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf", + "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0397 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae \u00abaddress[:port]\u00bb \u03b3\u03b9\u03b1 \u00absecure\u00bb \u03ba\u03b1\u03b9 \u00abnon-secure\u00bb. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '192.168.1.1'. \u0397 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03b9 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c9\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03b5 2101 \u03b3\u03b9\u03b1 \"non-secure\" \u03ba\u03b1\u03b9 2601 \u03b3\u03b9\u03b1 \"secure\". \u0393\u03b9\u03b1 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf, \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'tty[:baud]'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '/dev/ttyS1'. \u03a4\u03bf baud \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 115200.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" + }, "user": { + "data": { + "address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bf \u03c4\u03bf\u03bc\u03ad\u03b1\u03c2 \u03ae \u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b5\u03ac\u03bd \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "prefix": "\u0388\u03bd\u03b1 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b1\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 ElkM1).", + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf", + "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0397 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'address[:port]' \u03b3\u03b9\u03b1 'secure' \u03ba\u03b1\u03b9 'non-secure'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '192.168.1.1'. \u0397 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 2101 \u03b3\u03b9\u03b1 '\u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae' \u03ba\u03b1\u03b9 2601 \u03b3\u03b9\u03b1 '\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae'. \u0393\u03b9\u03b1 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf, \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'tty[:baud]'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '/dev/ttyS1'. \u03a4\u03bf baud \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 115200.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" } diff --git a/homeassistant/components/elkm1/translations/en.json b/homeassistant/components/elkm1/translations/en.json index 04fd3c189b562..3b1993f3fedf0 100644 --- a/homeassistant/components/elkm1/translations/en.json +++ b/homeassistant/components/elkm1/translations/en.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "An ElkM1 with this address is already configured", - "already_configured": "An ElkM1 with this prefix is already configured" + "already_configured": "An ElkM1 with this prefix is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "Password", + "protocol": "Protocol", + "temperature_unit": "The temperature unit ElkM1 uses.", + "username": "Username" + }, + "description": "Connect to the discovered system: {mac_address} ({host})", + "title": "Connect to Elk-M1 Control" + }, + "manual_connection": { "data": { "address": "The IP address or domain or serial port if connecting via serial.", "password": "Password", @@ -21,6 +34,19 @@ }, "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", "title": "Connect to Elk-M1 Control" + }, + "user": { + "data": { + "address": "The IP address or domain or serial port if connecting via serial.", + "device": "Device", + "password": "Password", + "prefix": "A unique prefix (leave blank if you only have one ElkM1).", + "protocol": "Protocol", + "temperature_unit": "The temperature unit ElkM1 uses.", + "username": "Username" + }, + "description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.", + "title": "Connect to Elk-M1 Control" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index eaf987f95e65b..06c0d9e257e64 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -2,17 +2,43 @@ "config": { "abort": { "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", - "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo" + "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo", + "already_in_progress": "La configuraci\u00f3n ya se encuentra en proceso", + "cannot_connect": "Error al conectar" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Contrase\u00f1a", + "protocol": "Protocolo", + "temperature_unit": "La unidad de temperatura que el ElkM1 usa.", + "username": "Usuario" + }, + "description": "Con\u00e9ctese al sistema detectado: {mac_address} ({host})", + "title": "Conectar con Control Elk-M1" + }, + "manual_connection": { + "data": { + "address": "La direcci\u00f3n IP o el dominio o el puerto serie si se conecta a trav\u00e9s de serie.", + "password": "Contrase\u00f1a", + "prefix": "Un prefijo \u00fanico (dejar en blanco si solo tiene un ElkM1).", + "protocol": "Protocolo", + "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", + "username": "Usuario" + }, + "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "title": "Conectar con Control Elk-M1" + }, "user": { "data": { "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", + "device": "Dispositivo", "password": "Contrase\u00f1a", "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si s\u00f3lo tienes un Elk-M1).", "protocol": "Protocolo", diff --git a/homeassistant/components/elkm1/translations/et.json b/homeassistant/components/elkm1/translations/et.json index 7ced75e0a2be9..d874763045f8e 100644 --- a/homeassistant/components/elkm1/translations/et.json +++ b/homeassistant/components/elkm1/translations/et.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "Selle aadressiga ElkM1 on juba seadistatud", - "already_configured": "Selle eesliitega ElkM1 on juba seadistatud" + "already_configured": "Selle eesliitega ElkM1 on juba seadistatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "cannot_connect": "\u00dchendumine nurjus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Salas\u00f5na", + "protocol": "Protokoll", + "temperature_unit": "Temperatuuri\u00fchik mida ElkM1 kasutab.", + "username": "Kasutajanimi" + }, + "description": "\u00dchendu avastatud s\u00fcsteemiga: {mac_address} ( {host} )", + "title": "\u00dchendu Elk-M1 Controliga" + }, + "manual_connection": { + "data": { + "address": "IP-aadress v\u00f5i domeen v\u00f5i jadaport kui \u00fchendus toimub jadapordi kaudu.", + "password": "Salas\u00f5na", + "prefix": "Unikaalne eesliide (j\u00e4ta t\u00fchjaks kui sul on ainult \u00fcks ElkM1).", + "protocol": "Protokoll", + "temperature_unit": "Temperatuuri\u00fchik mida ElkM1 kasutab.", + "username": "Kasutajanimi" + }, + "description": "Turvalise ja mitteturvalise aadressi puhul peab aadressi string olema kujul 'address[:port]'. N\u00e4ide: '192.168.1.1'. Port on valikuline ja vaikimisi on see 2101 \"mitteturvalise\" ja 2601 \"turvalise\" puhul. Seeriaprotokolli puhul peab aadress olema kujul 'tty[:baud]'. N\u00e4ide: '/dev/ttyS1'. Baud on valikuline ja vaikimisi 115200.", + "title": "\u00dchendu Elk-M1 Controliga" + }, "user": { "data": { "address": "IP-aadress v\u00f5i domeen v\u00f5i jadaport, kui \u00fchendatakse jadaliidese kaudu.", + "device": "Seade", "password": "Salas\u00f5na", "prefix": "Unikaalne eesliide (j\u00e4ta t\u00fchjaks kui on ainult \u00fcks ElkM1).", "protocol": "Protokoll", "temperature_unit": "ElkM1'i temperatuuri\u00fchik.", "username": "Kasutajanimi" }, - "description": "Aadressistring peab olema kujul \"aadress[:port]\" \"secure\" ja \"non-secure\" puhul. N\u00e4ide: \"192.168.1.1\". Port on valikuline ja vaikimisi 2101 \"secure\" ja 2601 \"non-secure puhul\". Jadaprotokolli puhul peab aadress olema kujul \"tty[:baud]\". N\u00e4ide: \"/dev/ttyS1\". Baud on valikuline ja vaikimisi 115200.", + "description": "Vali avastatud s\u00fcsteem v\u00f5i \"K\u00e4sitsi sisestamine\" kui \u00fchtegi seadet ei ole avastatud.", "title": "\u00dchendu Elk-M1 Control" } } diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 665ac4b4d92d5..87193a9adf7d8 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "Un ElkM1 avec cette adresse est d\u00e9j\u00e0 configur\u00e9", - "already_configured": "Un ElkM1 avec ce pr\u00e9fixe est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Un ElkM1 avec ce pr\u00e9fixe est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion" }, "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Mot de passe", + "protocol": "Protocole", + "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.", + "username": "Nom d'utilisateur" + }, + "description": "Connectez-vous au syst\u00e8me d\u00e9couvert : {mac_address} ({host})", + "title": "Se connecter a Elk-M1 Control" + }, + "manual_connection": { + "data": { + "address": "L'adresse IP ou le domaine ou le port s\u00e9rie en cas de connexion via le port s\u00e9rie.", + "password": "Mot de passe", + "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", + "protocol": "Protocole", + "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.", + "username": "Nom d'utilisateur" + }, + "description": "La cha\u00eene d'adresse doit \u00eatre au format 'adresse[:port]' pour 's\u00e9curis\u00e9' et 'non s\u00e9curis\u00e9'. Exemple : '192.168.1.1'. Le port est facultatif et sa valeur par d\u00e9faut est 2101 pour \"non s\u00e9curis\u00e9\" et 2601 pour \"s\u00e9curis\u00e9\". Pour le protocole s\u00e9rie, l'adresse doit \u00eatre sous la forme 'tty[:baud]'. Exemple : '/dev/ttyS1'. Le baud est facultatif et sa valeur par d\u00e9faut est 115200.", + "title": "Se connecter a Elk-M1 Control" + }, "user": { "data": { "address": "L'adresse IP ou le domaine ou le port s\u00e9rie si vous vous connectez via s\u00e9rie.", + "device": "Appareil", "password": "Mot de passe", "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", "protocol": "Protocole", "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.", "username": "Nom d'utilisateur" }, - "description": "La cha\u00eene d'adresse doit \u00eatre au format \u00abadresse [: port]\u00bb pour \u00abs\u00e9curis\u00e9\u00bb et \u00abnon s\u00e9curis\u00e9\u00bb. Exemple: '192.168.1.1'. Le port est facultatif et vaut par d\u00e9faut 2101 pour \u00abnon s\u00e9curis\u00e9\u00bb et 2601 pour \u00abs\u00e9curis\u00e9\u00bb. Pour le protocole s\u00e9rie, l'adresse doit \u00eatre au format \u00abtty [: baud]\u00bb. Exemple: '/ dev / ttyS1'. Le baud est facultatif et par d\u00e9faut \u00e0 115200.", + "description": "Choisissez un syst\u00e8me d\u00e9couvert ou 'Entr\u00e9e manuelle' si aucun appareil n'a \u00e9t\u00e9 d\u00e9couvert.", "title": "Se connecter a Elk-M1 Control" } } diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json index e85bab17ac036..eb49e33c01943 100644 --- a/homeassistant/components/elkm1/translations/he.json +++ b/homeassistant/components/elkm1/translations/he.json @@ -1,17 +1,40 @@ { "config": { + "abort": { + "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" + }, "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" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" + }, + "manual_connection": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" + }, "user": { "data": { + "device": "\u05d4\u05ea\u05e7\u05df", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" } } } diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json index ff6445f0b728b..d076ad684c246 100644 --- a/homeassistant/components/elkm1/translations/hu.json +++ b/homeassistant/components/elkm1/translations/hu.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "Az ElkM1 ezzel a c\u00edmmel m\u00e1r konfigur\u00e1lva van", - "already_configured": "Az ezzel az el\u0151taggal rendelkez\u0151 ElkM1 m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az ezzel az el\u0151taggal rendelkez\u0151 ElkM1 m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Jelsz\u00f3", + "protocol": "Protokoll", + "temperature_unit": "Az ElkM1 \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g.", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Csatlakoz\u00e1s a felfedezett rendszerhez: {mac_address} ({host})", + "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" + }, + "manual_connection": { + "data": { + "address": "Az IP-c\u00edm vagy tartom\u00e1ny vagy soros port, ha soros kapcsolaton kereszt\u00fcl csatlakozik.", + "password": "Jelsz\u00f3", + "prefix": "Egyedi el\u0151tag (hagyja \u00fcresen, ha csak egy ElkM1 van).", + "protocol": "Protokoll", + "temperature_unit": "Az ElkM1 \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g.", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "A c\u00edmsornak a \"biztons\u00e1gos\" \u00e9s a \"nem biztons\u00e1gos\" eset\u00e9ben a \"address[:port]\" form\u00e1j\u00fanak kell lennie. P\u00e9lda: '192.168.1.1'. A port megad\u00e1sa opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 2101 a \"nem biztons\u00e1gos\" \u00e9s 2601 a \"biztons\u00e1gos\" eset\u00e9ben. A soros protokoll eset\u00e9ben a c\u00edmnek a 'tty[:baud]' form\u00e1j\u00fanak kell lennie. P\u00e9lda: '/dev/ttyS1'. A baud nem k\u00f6telez\u0151, \u00e9s alap\u00e9rtelmez\u00e9s szerint 115200.", + "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" + }, "user": { "data": { "address": "Az IP-c\u00edm vagy tartom\u00e1ny vagy soros port, ha soros kapcsolaton kereszt\u00fcl csatlakozik.", + "device": "Eszk\u00f6z", "password": "Jelsz\u00f3", "prefix": "Egyedi el\u0151tag (hagyja \u00fcresen, ha csak egy ElkM1 van).", "protocol": "Protokoll", "temperature_unit": "Az ElkM1 h\u0151m\u00e9rs\u00e9kleti egys\u00e9g haszn\u00e1lja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A c\u00edmsornak a \u201ebiztons\u00e1gos\u201d \u00e9s a \u201enem biztons\u00e1gos\u201d \u201ec\u00edm [: port]\u201d form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '192.168.1.1'. A port opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 2101 \u201enem biztons\u00e1gos\u201d \u00e9s 2601 \u201ebiztons\u00e1gos\u201d. A soros protokollhoz a c\u00edmnek 'tty [: baud]' form\u00e1tum\u00fanak kell lennie. P\u00e9lda: '/dev/ttyS1'. A baud opcion\u00e1lis, \u00e9s alap\u00e9rtelmez\u00e9s szerint 115200.", + "description": "V\u00e1lasszon egy felfedezett rendszert vagy a \u201eK\u00e9zi bevitelt\u201d, ha nem \u00e9szlelt eszk\u00f6zt.", "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" } } diff --git a/homeassistant/components/elkm1/translations/id.json b/homeassistant/components/elkm1/translations/id.json index e7ddd3cf9ee4e..782906fac0ab1 100644 --- a/homeassistant/components/elkm1/translations/id.json +++ b/homeassistant/components/elkm1/translations/id.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "ElkM1 dengan alamat ini sudah dikonfigurasi", - "already_configured": "ElkM1 dengan prefiks ini sudah dikonfigurasi" + "already_configured": "ElkM1 dengan prefiks ini sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "Kata Sandi", + "protocol": "Protokol", + "temperature_unit": "Unit suhu yang digunakan ElkM1.", + "username": "Nama Pengguna" + }, + "description": "Hubungkan ke sistem yang ditemukan: {mac_address} ({host})", + "title": "Hubungkan ke Kontrol Elk-M1" + }, + "manual_connection": { "data": { "address": "Alamat IP atau domain atau port serial jika terhubung melalui serial.", "password": "Kata Sandi", @@ -21,6 +34,19 @@ }, "description": "String alamat harus dalam format 'alamat[:port]' untuk 'aman' dan 'tidak aman'. Misalnya, '192.168.1.1'. Port bersifat opsional dan nilai baku adalah 2101 untuk 'tidak aman' dan 2601 untuk 'aman'. Untuk protokol serial, alamat harus dalam format 'tty[:baud]'. Misalnya, '/dev/ttyS1'. Baud bersifat opsional dan nilai bakunya adalah 115200.", "title": "Hubungkan ke Kontrol Elk-M1" + }, + "user": { + "data": { + "address": "Alamat IP atau domain atau port serial jika terhubung melalui serial.", + "device": "Perangkat", + "password": "Kata Sandi", + "prefix": "Prefiks unik (kosongkan jika hanya ada satu ElkM1).", + "protocol": "Protokol", + "temperature_unit": "Unit suhu yang digunakan ElkM1.", + "username": "Nama Pengguna" + }, + "description": "Pilih sistem yang ditemukan atau 'Entri Manual' jika tidak ada perangkat yang ditemukan.", + "title": "Hubungkan ke Kontrol Elk-M1" } } } diff --git a/homeassistant/components/elkm1/translations/it.json b/homeassistant/components/elkm1/translations/it.json index b22ba8c852878..fa05ab5d436c6 100644 --- a/homeassistant/components/elkm1/translations/it.json +++ b/homeassistant/components/elkm1/translations/it.json @@ -2,25 +2,51 @@ "config": { "abort": { "address_already_configured": "Un ElkM1 con questo indirizzo \u00e8 gi\u00e0 configurato", - "already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato" + "already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi" }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Password", + "protocol": "Protocollo", + "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", + "username": "Nome utente" + }, + "description": "Connetti al sistema rilevato: {mac_address} ({host})", + "title": "Connettiti al controllo Elk-M1" + }, + "manual_connection": { + "data": { + "address": "L'indirizzo IP o il dominio o la porta seriale in caso di connessione tramite seriale.", + "password": "Password", + "prefix": "Un prefisso univoco (lascia vuoto se hai solo un ElkM1).", + "protocol": "Protocollo", + "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", + "username": "Nome utente" + }, + "description": "La stringa dell'indirizzo deve essere nel formato 'indirizzo[:porta]' per 'sicuro' e 'non-sicuro'. Esempio: '192.168.1.1'. La porta \u00e8 facoltativa e per impostazione predefinita \u00e8 2101 per 'non-sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nel formato 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.", + "title": "Connettiti al controllo Elk-M1" + }, "user": { "data": { "address": "L'indirizzo IP o il dominio o la porta seriale se ci si connette tramite seriale.", + "device": "Dispositivo", "password": "Password", "prefix": "Un prefisso univoco (lascia vuoto se disponi di un solo ElkM1).", "protocol": "Protocollo", "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", "username": "Nome utente" }, - "description": "La stringa di indirizzi deve essere nella forma 'indirizzo[:porta]' per 'sicuro' e 'non sicuro'. Esempio: '192.168.1.1.1'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101 per 'non sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.", - "title": "Collegamento al controllo Elk-M1" + "description": "Scegli un sistema rilevato o \"Inserimento manuale\" se non sono stati rilevati dispositivi.", + "title": "Connettiti al controllo Elk-M1" } } } diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index a2dea3c10cfba..f280d6d6276cf 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -2,17 +2,43 @@ "config": { "abort": { "address_already_configured": "\u3053\u306e\u30a2\u30c9\u30ec\u30b9\u306eElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_configured": "\u3053\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u6301\u3064ElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u3053\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u6301\u3064ElkM1\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", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "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" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", + "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "\u691c\u51fa\u3055\u308c\u305f\u30b7\u30b9\u30c6\u30e0\u306b\u63a5\u7d9a\u3057\u307e\u3059: {mac_address} ({host})", + "title": "Elk-M1 Control\u306b\u63a5\u7d9a" + }, + "manual_connection": { + "data": { + "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u3082\u3057\u304f\u306f\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306b\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "prefix": "\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(\u63a5\u982d\u8f9e)(ElkM1\u304c1\u3064\u306e\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e)", + "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", + "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "\u30a2\u30c9\u30ec\u30b9\u6587\u5b57\u5217\u306f\u3001 '\u30bb\u30ad\u30e5\u30a2 '\u304a\u3088\u3073 '\u975e\u30bb\u30ad\u30e5\u30a2 '\u306e\u5834\u5408\u306f\u3001'address[:port]'\u306e\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: '192.168.1.1'\u3002\u30dd\u30fc\u30c8\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f'\u975e\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012101 \u3067'\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012601 \u3067\u3059\u3002\u30b7\u30ea\u30a2\u30eb\u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u5834\u5408\u3001\u30a2\u30c9\u30ec\u30b9\u306f\u3001'tty[:baud]' \u306e\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: '/dev/ttyS1'\u3002\u30dc\u30fc(baud)\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f115200\u3067\u3059\u3002", + "title": "Elk-M1 Control\u306b\u63a5\u7d9a" + }, "user": { "data": { "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", + "device": "\u30c7\u30d0\u30a4\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "prefix": "\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(ElkM1\u304c1\u3064\u3057\u304b\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json index de51e67b2062c..c3efd45dbc069 100644 --- a/homeassistant/components/elkm1/translations/nl.json +++ b/homeassistant/components/elkm1/translations/nl.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "Een ElkM1 met dit adres is al geconfigureerd", - "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd" + "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken" }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "Wachtwoord", + "protocol": "Protocol", + "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", + "username": "Gebruikersnaam" + }, + "description": "Maak verbinding met het ontdekte systeem: {mac_address} ({host})", + "title": "Maak verbinding met Elk-M1 Control" + }, + "manual_connection": { "data": { "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", "password": "Wachtwoord", @@ -21,6 +34,19 @@ }, "description": "De adresreeks moet de vorm 'adres [: poort]' hebben voor 'veilig' en 'niet-beveiligd'. Voorbeeld: '192.168.1.1'. De poort is optioneel en is standaard 2101 voor 'niet beveiligd' en 2601 voor 'beveiligd'. Voor het seri\u00eble protocol moet het adres de vorm 'tty [: baud]' hebben. Voorbeeld: '/ dev / ttyS1'. De baud is optioneel en is standaard ingesteld op 115200.", "title": "Maak verbinding met Elk-M1 Control" + }, + "user": { + "data": { + "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", + "device": "Apparaat", + "password": "Wachtwoord", + "prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).", + "protocol": "Protocol", + "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", + "username": "Gebruikersnaam" + }, + "description": "Kies een ontdekt systeem of 'Handmatige invoer' als er geen apparaten zijn ontdekt.", + "title": "Maak verbinding met Elk-M1 Control" } } } diff --git a/homeassistant/components/elkm1/translations/no.json b/homeassistant/components/elkm1/translations/no.json index 39452b5809471..ced77ff0d046e 100644 --- a/homeassistant/components/elkm1/translations/no.json +++ b/homeassistant/components/elkm1/translations/no.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "En ElkM1 med denne adressen er allerede konfigurert", - "already_configured": "En ElkM1 med dette prefikset er allerede konfigurert" + "already_configured": "En ElkM1 med dette prefikset er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes" }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, + "flow_title": "{mac_address} ( {host} )", "step": { + "discovered_connection": { + "data": { + "password": "Passord", + "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 bruker.", + "username": "Brukernavn" + }, + "description": "Koble til det oppdagede systemet: {mac_address} ( {host} )", + "title": "Koble til Elk-M1-kontroll" + }, + "manual_connection": { + "data": { + "address": "IP-adressen eller domene- eller serieporten hvis du kobler til via seriell.", + "password": "Passord", + "prefix": "Et unikt prefiks (la det st\u00e5 tomt hvis du bare har \u00e9n ElkM1).", + "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 bruker.", + "username": "Brukernavn" + }, + "description": "Adressestrengen m\u00e5 ha formen 'adresse[:port]' for 'sikker' og 'ikke-sikker'. Eksempel: '192.168.1.1'. Porten er valgfri og er standard til 2101 for \"ikke-sikker\" og 2601 for \"sikker\". For serieprotokollen m\u00e5 adressen v\u00e6re i formen 'tty[:baud]'. Eksempel: '/dev/ttyS1'. Bauden er valgfri og er standard til 115200.", + "title": "Koble til Elk-M1-kontroll" + }, "user": { "data": { "address": "IP-adressen eller domenet eller seriell port hvis du kobler til via seriell.", + "device": "Enhet", "password": "Passord", "prefix": "Et unikt prefiks (la v\u00e6re tomt hvis du bare har en ElkM1).", "protocol": "Protokoll", "temperature_unit": "Temperaturenheten ElkM1 bruker.", "username": "Brukernavn" }, - "description": "Adressestrengen m\u00e5 v\u00e6re i formen 'adresse [: port]' for 'sikker' og 'ikke-sikker'. Eksempel: '192.168.1.1'. Porten er valgfri og er standard til 2101 for 'ikke-sikker' og 2601 for 'sikker'. For den serielle protokollen m\u00e5 adressen v\u00e6re i formen 'tty [: baud]'. Eksempel: '/ dev / ttyS1'. Baud er valgfri og er standard til 115200.", + "description": "Velg et oppdaget system eller \"Manuell oppf\u00f8ring\" hvis ingen enheter har blitt oppdaget.", "title": "Koble til Elk-M1-kontroll" } } diff --git a/homeassistant/components/elkm1/translations/pl.json b/homeassistant/components/elkm1/translations/pl.json index b9c0322af2090..62900716f62e7 100644 --- a/homeassistant/components/elkm1/translations/pl.json +++ b/homeassistant/components/elkm1/translations/pl.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane z tym adresem", - "already_configured": "ElkM1 z tym prefiksem jest ju\u017c skonfigurowany" + "already_configured": "ElkM1 z tym prefiksem jest ju\u017c skonfigurowany", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Has\u0142o", + "protocol": "Protok\u00f3\u0142", + "temperature_unit": "Jednostka temperatury u\u017cywana przez ElkM1.", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Po\u0142\u0105cz si\u0119 z wykrytym systemem: {mac_address} ({host})", + "title": "Pod\u0142\u0105czenie do sterownika Elk-M1" + }, + "manual_connection": { + "data": { + "address": "Adres IP, domena lub port szeregowy w przypadku po\u0142\u0105czenia szeregowego.", + "password": "Has\u0142o", + "prefix": "Unikalny prefiks (pozostaw puste, je\u015bli masz tylko jedno urz\u0105dzenie ElkM1)", + "protocol": "Protok\u00f3\u0142", + "temperature_unit": "Jednostka temperatury u\u017cywana przez ElkM1.", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Adres musi by\u0107 w postaci 'adres[:port]' dla tryb\u00f3w 'zabezpieczony' i 'niezabezpieczony'. Przyk\u0142ad: '192.168.1.1'. Port jest opcjonalny i domy\u015blnie ustawiony na 2101 dla po\u0142\u0105cze\u0144 'niezabezpieczonych' i 2601 dla 'zabezpieczonych'. W przypadku protoko\u0142u szeregowego adres musi by\u0107 w formie 'tty[:baudrate]'. Przyk\u0142ad: '/dev/ttyS1'. Warto\u015b\u0107 transmisji jest opcjonalna i domy\u015blnie wynosi 115200.", + "title": "Pod\u0142\u0105czenie do sterownika Elk-M1" + }, "user": { "data": { "address": "Adres IP, domena lub port szeregowy w przypadku po\u0142\u0105czenia szeregowego.", + "device": "Urz\u0105dzenie", "password": "Has\u0142o", "prefix": "Unikatowy prefiks (pozostaw pusty, je\u015bli masz tylko jeden ElkM1).", "protocol": "Protok\u00f3\u0142", "temperature_unit": "Jednostka temperatury u\u017cywanej przez ElkM1.", "username": "Nazwa u\u017cytkownika" }, - "description": "Adres musi by\u0107 w postaci 'adres[:port]' dla tryb\u00f3w 'zabezpieczony' i 'niezabezpieczony'. Przyk\u0142ad: '192.168.1.1'. Port jest opcjonalny i domy\u015blnie ustawiony na 2101 dla po\u0142\u0105cze\u0144 'niezabezpieczonych' i 2601 dla 'zabezpieczonych'. W przypadku protoko\u0142u szeregowego adres musi by\u0107 w formie 'tty[:baudrate]'. Przyk\u0142ad: '/dev/ttyS1'. Warto\u015b\u0107 transmisji jest opcjonalna i domy\u015blnie wynosi 115200.", + "description": "Wybierz wykryty system lub \u201eWpis r\u0119czny\u201d, je\u015bli nie wykryto \u017cadnych urz\u0105dze\u0144.", "title": "Pod\u0142\u0105czenie do sterownika Elk-M1" } } diff --git a/homeassistant/components/elkm1/translations/pt-BR.json b/homeassistant/components/elkm1/translations/pt-BR.json index 932b4b8a72e0a..7cb9a5f101a74 100644 --- a/homeassistant/components/elkm1/translations/pt-BR.json +++ b/homeassistant/components/elkm1/translations/pt-BR.json @@ -1,10 +1,52 @@ { "config": { + "abort": { + "address_already_configured": "Um ElkM1 com este endere\u00e7o j\u00e1 est\u00e1 configurado", + "already_configured": "A conta j\u00e1 foi configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "Senha", + "protocol": "Protocolo", + "temperature_unit": "A unidade de temperatura que ElkM1 usa.", + "username": "Usu\u00e1rio" + }, + "description": "Conecte-se ao sistema descoberto: {mac_address} ({host})", + "title": "Conecte ao controle Elk-M1" + }, + "manual_connection": { + "data": { + "address": "O endere\u00e7o IP, dom\u00ednio ou porta serial se estiver conectando via serial.", + "password": "Senha", + "prefix": "Um prefixo exclusivo (deixe em branco se voc\u00ea tiver apenas um ElkM1).", + "protocol": "Protocolo", + "temperature_unit": "A unidade de temperatura que ElkM1 usa.", + "username": "Usu\u00e1rio" + }, + "description": "A string de endere\u00e7o deve estar no formato 'address[:port]' para 'seguro' e 'n\u00e3o seguro'. Exemplo: '192.168.1.1'. A porta \u00e9 opcional e o padr\u00e3o \u00e9 2101 para 'n\u00e3o seguro' e 2601 para 'seguro'. Para o protocolo serial, o endere\u00e7o deve estar no formato 'tty[:baud]'. Exemplo: '/dev/ttyS1'. O baud \u00e9 opcional e o padr\u00e3o \u00e9 115200.", + "title": "Conecte ao controle Elk-M1" + }, "user": { "data": { + "address": "O endere\u00e7o IP ou dom\u00ednio ou porta serial se estiver conectando via serial.", + "device": "Dispositivo", + "password": "Senha", + "prefix": "Um prefixo exclusivo (deixe em branco se voc\u00ea tiver apenas um ElkM1).", + "protocol": "Protocolo", + "temperature_unit": "A unidade de temperatura que ElkM1 usa.", "username": "Usu\u00e1rio" - } + }, + "description": "Escolha um sistema descoberto ou 'Entrada Manual' se nenhum dispositivo foi descoberto.", + "title": "Conecte ao controle Elk-M1" } } } diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json index 954722ecf5267..1b4bf7250d7f3 100644 --- a/homeassistant/components/elkm1/translations/ru.json +++ b/homeassistant/components/elkm1/translations/ru.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { "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." }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435: {mac_address} ({host})", + "title": "Elk-M1 Control" + }, + "manual_connection": { "data": { "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -21,6 +34,19 @@ }, "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 'secure' \u0438 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'non-secure' \u0438 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 115200.", "title": "Elk-M1 Control" + }, + "user": { + "data": { + "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1)", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0438\u043b\u0438 'Manual Entry', \u0435\u0441\u043b\u0438 \u043d\u0438\u043a\u0430\u043a\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", + "title": "Elk-M1 Control" } } } diff --git a/homeassistant/components/elkm1/translations/sk.json b/homeassistant/components/elkm1/translations/sk.json new file mode 100644 index 0000000000000..0b7bf878ea988 --- /dev/null +++ b/homeassistant/components/elkm1/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json index 3b62ad5e07991..e8f147c8d5798 100644 --- a/homeassistant/components/elkm1/translations/tr.json +++ b/homeassistant/components/elkm1/translations/tr.json @@ -2,15 +2,28 @@ "config": { "abort": { "address_already_configured": "Bu adrese sahip bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", - "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r" + "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, + "flow_title": "{mac_address} ({host})", "step": { - "user": { + "discovered_connection": { + "data": { + "password": "Parola", + "protocol": "Protokol", + "temperature_unit": "ElkM1'in kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi.", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Ke\u015ffedilen sisteme ba\u011flan\u0131n: {mac_address} ( {host} )", + "title": "Elk-M1 Kontrol\u00fcne Ba\u011flan\u0131n" + }, + "manual_connection": { "data": { "address": "Seri yoluyla ba\u011flan\u0131l\u0131yorsa IP adresi veya etki alan\u0131 veya seri ba\u011flant\u0131 noktas\u0131.", "password": "Parola", @@ -21,6 +34,19 @@ }, "description": "Adres dizesi, 'g\u00fcvenli' ve 'g\u00fcvenli olmayan' i\u00e7in 'adres[:port]' bi\u00e7iminde olmal\u0131d\u0131r. \u00d6rnek: '192.168.1.1'. Ba\u011flant\u0131 noktas\u0131 iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 'g\u00fcvenli olmayan' i\u00e7in 2101 ve 'g\u00fcvenli' i\u00e7in 2601'dir. Seri protokol i\u00e7in adres 'tty[:baud]' bi\u00e7iminde olmal\u0131d\u0131r. \u00d6rnek: '/dev/ttyS1'. Baud iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 115200'd\u00fcr.", "title": "Elk-M1 Kontrol\u00fcne Ba\u011flan\u0131n" + }, + "user": { + "data": { + "address": "Seri yoluyla ba\u011flan\u0131l\u0131yorsa IP adresi veya etki alan\u0131 veya seri ba\u011flant\u0131 noktas\u0131.", + "device": "Cihaz", + "password": "Parola", + "prefix": "Benzersiz bir \u00f6nek (yaln\u0131zca bir ElkM1'iniz varsa bo\u015f b\u0131rak\u0131n).", + "protocol": "Protokol", + "temperature_unit": "ElkM1'in kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi.", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Ke\u015ffedilen bir sistem veya ke\u015ffedilmemi\u015fse 'Manuel Giri\u015f' se\u00e7in.", + "title": "Elk-M1 Kontrol\u00fcne Ba\u011flan\u0131n" } } } diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json index 0a2f1f60faac0..7b25413c6fc53 100644 --- a/homeassistant/components/elkm1/translations/zh-Hant.json +++ b/homeassistant/components/elkm1/translations/zh-Hant.json @@ -2,24 +2,50 @@ "config": { "abort": { "address_already_configured": "\u4f7f\u7528\u6b64\u4f4d\u5740\u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_configured": "\u4f7f\u7528\u6b64 Prefix \u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u4f7f\u7528\u6b64 Prefix \u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "{mac_address} ({host})", "step": { + "discovered_connection": { + "data": { + "password": "\u5bc6\u78bc", + "protocol": "\u901a\u8a0a\u5354\u5b9a", + "temperature_unit": "ElkM1 \u6240\u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u9023\u7dda\u81f3\u6240\u63a2\u7d22\u7684\u7cfb\u7d71\uff1a{mac_address} ({host})", + "title": "\u9023\u7dda\u81f3 Elk-M1 Control" + }, + "manual_connection": { + "data": { + "address": "IP \u6216\u7db2\u57df\u540d\u7a31\u3001\u5e8f\u5217\u57e0\uff08\u5047\u5982\u900f\u904e\u5e8f\u5217\u9023\u7dda\uff09\u3002", + "password": "\u5bc6\u78bc", + "prefix": "\u7368\u4e00\u7684 Prefix\uff08\u5047\u5982\u50c5\u6709\u4e00\u7d44 ElkM1 \u5247\u4fdd\u7559\u7a7a\u767d\uff09\u3002", + "protocol": "\u901a\u8a0a\u5354\u5b9a", + "temperature_unit": "ElkM1 \u6240\u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u52a0\u5bc6\u8207\u975e\u52a0\u5bc6\u4e4b\u4f4d\u5740\u5b57\u4e32\u683c\u5f0f\u5fc5\u9808\u70ba 'address[:port]'\u3002\u4f8b\u5982\uff1a'192.168.1.1'\u3002\u901a\u8a0a\u57e0\u70ba\u9078\u9805\u8f38\u5165\uff0c\u975e\u52a0\u5bc6\u9810\u8a2d\u503c\u70ba 2101\u3001\u52a0\u5bc6\u5247\u70ba 2601\u3002\u5e8f\u5217\u901a\u8a0a\u5354\u5b9a\u3001\u4f4d\u5740\u683c\u5f0f\u5fc5\u9808\u70ba 'tty[:baud]'\u3002\u4f8b\u5982\uff1a'/dev/ttyS1'\u3002\u50b3\u8f38\u7387\u70ba\u9078\u9805\u8f38\u5165\uff0c\u9810\u8a2d\u503c\u70ba 115200\u3002", + "title": "\u9023\u7dda\u81f3 Elk-M1 Control" + }, "user": { "data": { "address": "IP \u6216\u7db2\u57df\u540d\u7a31\u3001\u5e8f\u5217\u57e0\uff08\u5047\u5982\u900f\u904e\u5e8f\u5217\u9023\u7dda\uff09\u3002", + "device": "\u88dd\u7f6e", "password": "\u5bc6\u78bc", "prefix": "\u7368\u4e00\u7684 Prefix\uff08\u5047\u5982\u50c5\u6709\u4e00\u7d44 ElkM1 \u5247\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "protocol": "\u901a\u8a0a\u5354\u5b9a", "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u52a0\u5bc6\u8207\u975e\u52a0\u5bc6\u4e4b\u4f4d\u5740\u5b57\u4e32\u683c\u5f0f\u5fc5\u9808\u70ba 'address[:port]'\u3002\u4f8b\u5982\uff1a'192.168.1.1'\u3002\u901a\u8a0a\u57e0\u70ba\u9078\u9805\u8f38\u5165\uff0c\u975e\u52a0\u5bc6\u9810\u8a2d\u503c\u70ba 2101\u3001\u52a0\u5bc6\u5247\u70ba 2601\u3002\u5e8f\u5217\u901a\u8a0a\u5354\u5b9a\u3001\u4f4d\u5740\u683c\u5f0f\u5fc5\u9808\u70ba 'tty[:baud]'\u3002\u4f8b\u5982\uff1a'/dev/ttyS1'\u3002\u50b3\u8f38\u7387\u70ba\u9078\u9805\u8f38\u5165\uff0c\u9810\u8a2d\u503c\u70ba 115200\u3002", + "description": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" } } diff --git a/homeassistant/components/elmax/manifest.json b/homeassistant/components/elmax/manifest.json index b89ca55ce3db6..8e230dcab38dc 100644 --- a/homeassistant/components/elmax/manifest.json +++ b/homeassistant/components/elmax/manifest.json @@ -7,5 +7,6 @@ "codeowners": [ "@albertogeniola" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["elmax_api"] } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/el.json b/homeassistant/components/elmax/translations/el.json new file mode 100644 index 0000000000000..916ab58593507 --- /dev/null +++ b/homeassistant/components/elmax/translations/el.json @@ -0,0 +1,36 @@ +{ + "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": { + "bad_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_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_pin": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf pin \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", + "network_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "no_panel_online": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 Elmax.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "panels": { + "data": { + "panel_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03af\u03bd\u03b1\u03ba\u03b1", + "panel_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1", + "panel_pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b8\u03ad\u03bb\u03b1\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03af\u03bd\u03b1\u03ba\u03b1" + }, + "user": { + "data": { + "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": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud \u03c4\u03b7\u03c2 Elmax \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + } + } + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Elmax Cloud" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/lv.json b/homeassistant/components/elmax/translations/lv.json new file mode 100644 index 0000000000000..9cac0f2bb8ecc --- /dev/null +++ b/homeassistant/components/elmax/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "panels": { + "data": { + "panel_pin": "PIN kods" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/nb.json b/homeassistant/components/elmax/translations/nb.json new file mode 100644 index 0000000000000..f126937f2fea1 --- /dev/null +++ b/homeassistant/components/elmax/translations/nb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/pt-BR.json b/homeassistant/components/elmax/translations/pt-BR.json new file mode 100644 index 0000000000000..9db13c83fbc77 --- /dev/null +++ b/homeassistant/components/elmax/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "bad_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_pin": "O C\u00f3digo PIN fornecido \u00e9 inv\u00e1lido", + "network_error": "Ocorreu um erro de rede", + "no_panel_online": "Nenhum painel de controle on-line Elmax foi encontrado.", + "unknown": "Erro inesperado", + "unknown_error": "Erro inesperado" + }, + "step": { + "panels": { + "data": { + "panel_id": "ID do painel", + "panel_name": "Nome do painel", + "panel_pin": "C\u00f3digo PIN" + }, + "description": "Selecione qual painel voc\u00ea gostaria de controlar com esta integra\u00e7\u00e3o. Observe que o painel deve estar LIGADO para ser configurado.", + "title": "Sele\u00e7\u00e3o do painel" + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Fa\u00e7a login na nuvem Elmax usando suas credenciais", + "title": "Login da conta" + } + } + }, + "title": "Configura\u00e7\u00e3o de nuvem Elmax" +} \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/sk.json b/homeassistant/components/elmax/translations/sk.json new file mode 100644 index 0000000000000..ae37c2ed275da --- /dev/null +++ b/homeassistant/components/elmax/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown_error": "Vyskytla sa neo\u010dak\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index a5eb96e137681..2ee922442e698 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pca", "codeowners": ["@majuss"], "requirements": ["pypca==0.0.7"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pypca"] } diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 00c05702db706..f626ee165be37 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": ["pyemby==1.8"], "codeowners": ["@mezz64"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyemby"] } diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json index 331597225f005..6548c71171c13 100644 --- a/homeassistant/components/emonitor/manifest.json +++ b/homeassistant/components/emonitor/manifest.json @@ -4,7 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emonitor", "requirements": ["aioemonitor==1.0.5"], - "dhcp": [{ "hostname": "emonitor*", "macaddress": "0090C2*" }], + "dhcp": [ + {"hostname": "emonitor*", "macaddress": "0090C2*"}, + {"registered_devices": true} + ], "codeowners": ["@bdraco"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aioemonitor"] } diff --git a/homeassistant/components/emonitor/translations/el.json b/homeassistant/components/emonitor/translations/el.json index 8da0b8dbd4e7a..7a5ed2b14821a 100644 --- a/homeassistant/components/emonitor/translations/el.json +++ b/homeassistant/components/emonitor/translations/el.json @@ -1,10 +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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "flow_title": "{name}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SiteSage Emonitor" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } } } } diff --git a/homeassistant/components/emonitor/translations/pt-BR.json b/homeassistant/components/emonitor/translations/pt-BR.json new file mode 100644 index 0000000000000..80e47d1f10cea --- /dev/null +++ b/homeassistant/components/emonitor/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Deseja configurar {name} ({host})?", + "title": "Configura\u00e7\u00e3o SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index d8d0596989376..c11fecb3ff16d 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,8 +2,9 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.9.6"], + "requirements": ["sense_energy==0.10.2"], "codeowners": ["@kbickar"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["sense_energy"] } diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 36a86137e8725..c006a627f2f16 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -6,5 +6,6 @@ "requirements": ["emulated_roku==0.2.1"], "dependencies": ["network"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["emulated_roku"] } diff --git a/homeassistant/components/emulated_roku/translations/el.json b/homeassistant/components/emulated_roku/translations/el.json new file mode 100644 index 0000000000000..7815ff977ef3b --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/el.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "advertise_ip": "\u0394\u03b9\u03b1\u03c6\u03ae\u03bc\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 IP", + "advertise_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2", + "host_ip": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae", + "listen_port": "\u0391\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7 \u03b8\u03cd\u03c1\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "upnp_bind_multicast": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ae\u03c2 (\u03a3\u03c9\u03c3\u03c4\u03cc/\u039b\u03ac\u03b8\u03bf\u03c2)" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" + } + } + }, + "title": "Emulated Roku" +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/pt-BR.json b/homeassistant/components/emulated_roku/translations/pt-BR.json index b04554fd41eb6..139a17577e8bd 100644 --- a/homeassistant/components/emulated_roku/translations/pt-BR.json +++ b/homeassistant/components/emulated_roku/translations/pt-BR.json @@ -1,12 +1,15 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { "advertise_ip": "Anunciar IP", "advertise_port": "Anunciar porta", "host_ip": "IP do host", - "listen_port": "Porta de escuta", + "listen_port": "Ouvir Porta", "name": "Nome", "upnp_bind_multicast": "Vincular multicast (Verdadeiro/Falso)" }, @@ -14,5 +17,5 @@ } } }, - "title": "EmulatedRoku" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/sk.json b/homeassistant/components/emulated_roku/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index f8c14ed8b73b2..d07d340607364 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -291,7 +291,7 @@ async def async_update(self, update: EnergyPreferencesUpdate) -> None: "device_consumption", ): if key in update: - data[key] = update[key] # type: ignore + data[key] = update[key] # type: ignore[misc] self.data = data self._store.async_delay_save(lambda: cast(dict, self.data), 60) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index c0d9ffcea4a3e..f8591e5c23f78 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -148,17 +148,17 @@ async def finish() -> None: self._process_sensor_data( adapter, # Opting out of the type complexity because can't get it to work - energy_source, # type: ignore + energy_source, # type: ignore[arg-type] to_add, to_remove, ) continue - for flow in energy_source[adapter.flow_type]: # type: ignore + for flow in energy_source[adapter.flow_type]: # type: ignore[typeddict-item] self._process_sensor_data( adapter, # Opting out of the type complexity because can't get it to work - flow, # type: ignore + flow, # type: ignore[arg-type] to_add, to_remove, ) diff --git a/homeassistant/components/energy/translations/pt-BR.json b/homeassistant/components/energy/translations/pt-BR.json new file mode 100644 index 0000000000000..c8d85790fdd53 --- /dev/null +++ b/homeassistant/components/energy/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 37ed8a5c6bbc2..06bf8c7c0c843 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": ["openwebifpy==3.2.7"], "codeowners": ["@fbradyirl"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["openwebif"] } diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index 86db950ccc5d6..0fb4e9a9d3344 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -5,5 +5,6 @@ "requirements": ["enocean==0.50"], "codeowners": ["@bdurrer"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["enocean"] } diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 87bcd685a1f7c..e48f117648ab8 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -168,7 +168,7 @@ def value_changed(self, packet): # this packet reports the current value raw_val = packet.parsed["MR"]["raw_value"] divisor = packet.parsed["DIV"]["raw_value"] - self._attr_native_value = raw_val / (10 ** divisor) + self._attr_native_value = raw_val / (10**divisor) self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index b86b684455181..fc788e88d72ad 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -91,7 +91,7 @@ def value_changed(self, packet): if packet.parsed["DT"]["raw_value"] == 1: raw_val = packet.parsed["MR"]["raw_value"] divisor = packet.parsed["DIV"]["raw_value"] - watts = raw_val / (10 ** divisor) + watts = raw_val / (10**divisor) if watts > 1: self._on_state = True self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/translations/el.json b/homeassistant/components/enocean/translations/el.json new file mode 100644 index 0000000000000..1c1a8d1e2600b --- /dev/null +++ b/homeassistant/components/enocean/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae dongle", + "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": { + "invalid_dongle_path": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf dongle \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae" + }, + "step": { + "detect": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae dongle USB" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf ENOcean dongle" + }, + "manual": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae dongle USB" + }, + "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b3\u03b9\u03b1 \u03c4\u03bf dongle ENOcean" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/pt-BR.json b/homeassistant/components/enocean/translations/pt-BR.json new file mode 100644 index 0000000000000..c0d65a7934fad --- /dev/null +++ b/homeassistant/components/enocean/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Caminho de dongle inv\u00e1lido", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_dongle_path": "Nenhum dongle v\u00e1lido encontrado para este caminho" + }, + "step": { + "detect": { + "data": { + "path": "Caminho de dongle USB" + }, + "title": "Selecione o caminho para seu dongle ENOcean" + }, + "manual": { + "data": { + "path": "caminho do dongle USB" + }, + "title": "Digite o caminho para voc\u00ea dongle ENOcean" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/uk.json b/homeassistant/components/enocean/translations/uk.json index 5c3e2d6eb6ec9..01f7447c8aeb3 100644 --- a/homeassistant/components/enocean/translations/uk.json +++ b/homeassistant/components/enocean/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "invalid_dongle_path": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0437\u0430 \u0446\u0438\u043c \u0448\u043b\u044f\u0445\u043e\u043c." diff --git a/homeassistant/components/enocean/translations/zh-Hant.json b/homeassistant/components/enocean/translations/zh-Hant.json index 6000b968e5ec5..021b024c78ff9 100644 --- a/homeassistant/components/enocean/translations/zh-Hant.json +++ b/homeassistant/components/enocean/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\u88dd\u7f6e\u8def\u5f91\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u88dd\u7f6e" diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index d7ad10ca06223..a27b5a6bc79e3 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -14,5 +14,6 @@ "type": "_enphase-envoy._tcp.local." } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["envoy_reader"] } diff --git a/homeassistant/components/enphase_envoy/translations/el.json b/homeassistant/components/enphase_envoy/translations/el.json index f9852dcb28646..e2adc95e3aec4 100644 --- a/homeassistant/components/enphase_envoy/translations/el.json +++ b/homeassistant/components/enphase_envoy/translations/el.json @@ -1,8 +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", + "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", + "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" + }, "flow_title": "{serial} ({host})", "step": { "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0393\u03b9\u03b1 \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b1 \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03b1, \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 `envoy` \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0393\u03b9\u03b1 \u03c0\u03b1\u03bb\u03b1\u03b9\u03cc\u03c4\u03b5\u03c1\u03b1 \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03b1, \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 `installer` \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0393\u03b9\u03b1 \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03ac\u03bb\u03bb\u03b1 \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03b1, \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2." } } diff --git a/homeassistant/components/enphase_envoy/translations/nb.json b/homeassistant/components/enphase_envoy/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/pt-BR.json b/homeassistant/components/enphase_envoy/translations/pt-BR.json new file mode 100644 index 0000000000000..d4e296b6b4b7a --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{serial} ({host})", + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Para modelos mais novos, digite o nome de usu\u00e1rio `envoy` sem uma senha. Para modelos mais antigos, digite o nome de usu\u00e1rio `installer` sem uma senha. Para todos os outros modelos, insira um nome de usu\u00e1rio e senha v\u00e1lidos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sk.json b/homeassistant/components/enphase_envoy/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index 6f22689b9ca37..c7f4fbeef53df 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": ["enturclient==0.2.3"], "codeowners": ["@hfurubotten"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["enturclient"] } diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 868e62f07c34c..4d1f1ecdff0e6 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -5,5 +5,6 @@ "requirements": ["env_canada==0.5.20"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["env_canada"] } diff --git a/homeassistant/components/environment_canada/translations/el.json b/homeassistant/components/environment_canada/translations/el.json new file mode 100644 index 0000000000000..8cb9d4c62b66d --- /dev/null +++ b/homeassistant/components/environment_canada/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03ba\u03c5\u03c1\u03bf, \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b7 \u03b2\u03ac\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "error_response": "\u0391\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Environment Canada \u03ba\u03b1\u03c4\u03ac \u03bb\u03ac\u03b8\u03bf\u03c2", + "too_many_attempts": "\u039f\u03b9 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 \u03bc\u03b5 \u03c4\u03bf Environment Canada \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c3\u03b5 60 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "language": "\u0393\u03bb\u03ce\u03c3\u03c3\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "station": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bc\u03b5\u03c4\u03b5\u03c9\u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd" + }, + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03b5\u03af\u03c4\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03b5\u03af\u03c4\u03b5 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2/\u03bc\u03ae\u03ba\u03bf\u03c2. \u03a4\u03bf \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2/\u03bc\u03ae\u03ba\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf\u03b9 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \u039f \u03c0\u03bb\u03b7\u03c3\u03b9\u03ad\u03c3\u03c4\u03b5\u03c1\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b5\u03c9\u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ac\u03bd \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2. \u0395\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae: PP/\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2, \u03cc\u03c0\u03bf\u03c5 PP \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03b5\u03c0\u03b1\u03c1\u03c7\u03af\u03b1 \u03bc\u03b5 \u03b4\u03cd\u03bf \u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03b1 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd. \u0397 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c4\u03c9\u03bd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03b5\u03b4\u03ce: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. \u039f\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b1\u03b9\u03c1\u03cc \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03bf\u03cd\u03bd \u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 \u03b1\u03b3\u03b3\u03bb\u03b9\u03ba\u03ac \u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 \u03b3\u03b1\u03bb\u03bb\u03b9\u03ba\u03ac.", + "title": "Environment Canada: \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ba\u03b1\u03b9 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/pt-BR.json b/homeassistant/components/environment_canada/translations/pt-BR.json new file mode 100644 index 0000000000000..6325a1268237e --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "A ID da esta\u00e7\u00e3o \u00e9 inv\u00e1lida, ausente ou n\u00e3o encontrada no banco de dados de ID da esta\u00e7\u00e3o", + "cannot_connect": "Falha ao conectar", + "error_response": "Resposta do Environment Canada com erro", + "too_many_attempts": "As conex\u00f5es com o Environment Canada s\u00e3o limitadas por tarifas; Tente novamente em 60 segundos", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "language": "Idioma de informa\u00e7\u00f5es meteorol\u00f3gicas", + "latitude": "Latitude", + "longitude": "Longitude", + "station": "ID da esta\u00e7\u00e3o meteorol\u00f3gica" + }, + "description": "Um ID de esta\u00e7\u00e3o ou latitude/longitude deve ser especificado. A latitude/longitude padr\u00e3o usada s\u00e3o os valores configurados na instala\u00e7\u00e3o do Home Assistant. A esta\u00e7\u00e3o meteorol\u00f3gica mais pr\u00f3xima das coordenadas ser\u00e1 usada se especificar as coordenadas. Se for usado um c\u00f3digo de esta\u00e7\u00e3o, deve seguir o formato: PP/c\u00f3digo, onde PP \u00e9 a prov\u00edncia de duas letras e c\u00f3digo \u00e9 o ID da esta\u00e7\u00e3o. A lista de IDs de esta\u00e7\u00f5es pode ser encontrada aqui: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. As informa\u00e7\u00f5es meteorol\u00f3gicas podem ser recuperadas em ingl\u00eas ou franc\u00eas.", + "title": "Environment Canada: localiza\u00e7\u00e3o e idioma do clima" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/sk.json b/homeassistant/components/environment_canada/translations/sk.json new file mode 100644 index 0000000000000..e6945904d9030 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 52ac06ff8c30b..2154cd687726c 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": ["pyenvisalink==4.4"], "codeowners": ["@ufodone"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyenvisalink"] } diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 5abbc7b252a98..9d3047e442d3a 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": ["pyephember==0.3.1"], "codeowners": ["@ttroy50"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyephember"] } diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 069956bdc9a19..310b66c0d37a8 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": ["epson-projector==0.4.2"], "codeowners": ["@pszafer"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["epson_projector"] } diff --git a/homeassistant/components/epson/translations/el.json b/homeassistant/components/epson/translations/el.json index 12bff6f7b6c38..fbc101807eb5f 100644 --- a/homeassistant/components/epson/translations/el.json +++ b/homeassistant/components/epson/translations/el.json @@ -1,7 +1,16 @@ { "config": { "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "powered_off": "\u0395\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bf \u03c0\u03c1\u03bf\u03b2\u03bf\u03bb\u03ad\u03b1\u03c2; \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b2\u03b9\u03bd\u03c4\u03b5\u03bf\u03c0\u03c1\u03bf\u03b2\u03bf\u03bb\u03ad\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + }, + "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" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/epson/translations/pt-BR.json b/homeassistant/components/epson/translations/pt-BR.json new file mode 100644 index 0000000000000..c14278182a5bd --- /dev/null +++ b/homeassistant/components/epson/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "powered_off": "O projetor est\u00e1 ligado? Voc\u00ea precisa ligar o projetor para a configura\u00e7\u00e3o inicial." + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/sk.json b/homeassistant/components/epson/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/epson/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index 3fb7f1d598712..f16299ae47422 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "codeowners": ["@ThaStealth"], "requirements": ["epsonprinter==0.0.9"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["epsonprinter_pkg"] } diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 44329c95eb1c0..d514d54aa6662 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -167,8 +167,6 @@ def hvac_modes(self): def set_hvac_mode(self, hvac_mode): """Set operation mode.""" - if self.preset_mode: - return self._thermostat.mode = HA_TO_EQ_HVAC[hvac_mode] @property diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index a644ff394e035..4ad8d08adf552 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], "codeowners": ["@rytilahti"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bluepy", "eq3bt"] } diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7e799a83ee2f7..0154e2eba28c2 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -117,7 +117,7 @@ async def async_setup_entry( # noqa: C901 port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] noise_psk = entry.data.get(CONF_NOISE_PSK) - device_id = None + device_id: str | None = None zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -184,11 +184,12 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None: return # Call native tag scan - if service_name == "tag_scanned": + 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( - hass.components.tag.async_scan_tag(tag_id, device_id) - ) + hass.async_create_task(tag.async_scan_tag(tag_id, device_id)) return hass.bus.async_fire(service.service, service_data) @@ -343,18 +344,30 @@ def _async_setup_device_registry( sw_version = device_info.esphome_version if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" + configuration_url = None if device_info.webserver_port > 0: configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + + manufacturer = "espressif" + model = device_info.model + hw_version = None + if device_info.project_name: + project_name = device_info.project_name.split(".") + manufacturer = project_name[0] + model = project_name[1] + hw_version = device_info.project_version + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, - manufacturer="espressif", - model=device_info.model, + manufacturer=manufacturer, + model=model, sw_version=sw_version, + hw_version=hw_version, ) return device_entry.id @@ -445,7 +458,7 @@ async def _register_service( } async def execute_service(call: ServiceCall) -> None: - await entry_data.client.execute_service(service, call.data) # type: ignore[arg-type] + await entry_data.client.execute_service(service, call.data) hass.services.async_register( DOMAIN, service_name, execute_service, vol.Schema(schema) diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 4d9b769791c3e..cc82fd536e76f 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -1,4 +1,4 @@ -"""Diahgnostics support for ESPHome.""" +"""Diagnostics support for ESPHome.""" from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index e7bbc27141cb5..c00073b443202 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -19,6 +19,7 @@ EntityState, FanInfo, LightInfo, + LockInfo, NumberInfo, SelectInfo, SensorInfo, @@ -44,6 +45,7 @@ CoverInfo: "cover", FanInfo: "fan", LightInfo: "light", + LockInfo: "lock", NumberInfo: "number", SelectInfo: "select", SensorInfo: "sensor", diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py new file mode 100644 index 0000000000000..84c93d9df1343 --- /dev/null +++ b/homeassistant/components/esphome/lock.py @@ -0,0 +1,87 @@ +"""Support for ESPHome locks.""" +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import LockCommand, LockEntityState, LockInfo, LockState + +from homeassistant.components.lock import SUPPORT_OPEN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_CODE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ESPHome switches based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="lock", + info_type=LockInfo, + entity_type=EsphomeLock, + state_type=LockEntityState, + ) + + +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + +class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): + """A lock implementation for ESPHome.""" + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return self._static_info.assumed_state + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_OPEN if self._static_info.supports_open else 0 + + @property + def code_format(self) -> str | None: + """Regex for code format or None if no code is required.""" + if self._static_info.requires_code: + return self._static_info.code_format + return None + + @esphome_state_property + def is_locked(self) -> bool | None: + """Return true if the lock is locked.""" + return self._state.state == LockState.LOCKED + + @esphome_state_property + def is_locking(self) -> bool | None: + """Return true if the lock is locking.""" + return self._state.state == LockState.LOCKING + + @esphome_state_property + def is_unlocking(self) -> bool | None: + """Return true if the lock is unlocking.""" + return self._state.state == LockState.UNLOCKING + + @esphome_state_property + def is_jammed(self) -> bool | None: + """Return true if the lock is jammed (incomplete locking).""" + return self._state.state == LockState.JAMMED + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + await self._client.lock_command(self._static_info.key, LockCommand.LOCK) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + code = kwargs.get(ATTR_CODE, None) + await self._client.lock_command(self._static_info.key, LockCommand.UNLOCK, code) + + async def async_open(self, **kwargs: Any) -> None: + """Open the door latch.""" + await self._client.lock_command(self._static_info.key, LockCommand.OPEN) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 25e3abe970098..72a36076bf49e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -7,5 +7,6 @@ "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aioesphomeapi", "noiseprotocol"] } diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 952cb96fcd836..fbb22bb739702 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -5,7 +5,7 @@ from aioesphomeapi import SwitchInfo, SwitchState -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DEVICE_CLASSES, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,6 +45,13 @@ def is_on(self) -> bool | None: """Return true if the switch is on.""" return self._state.state + @property + def device_class(self) -> str | 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 + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._client.switch_command(self._static_info.key, True) diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index fc4a7d5bf8c45..c6885e0685126 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -8,6 +8,7 @@ "error": { "connection_error": "Nelze se p\u0159ipojit k ESP. Zkontrolujte, zda va\u0161e YAML konfigurace obsahuje \u0159\u00e1dek 'api:'.", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_psk": "Transportn\u00ed \u0161ifrovac\u00ed kl\u00ed\u010d je neplatn\u00fd. Ujist\u011bte se, \u017ee odpov\u00edd\u00e1 tomu, co m\u00e1te ve sv\u00e9 konfiguraci", "resolve_error": "Nelze naj\u00edt IP adresu uzlu ESP. Pokud tato chyba p\u0159etrv\u00e1v\u00e1, nastavte statickou adresu IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", @@ -16,12 +17,24 @@ "data": { "password": "Heslo" }, - "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} ." + "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name}." }, "discovery_confirm": { "description": "Chcete p\u0159idat uzel ESPHome `{name}` do Home Assistant?", "title": "Nalezen uzel ESPHome" }, + "encryption_key": { + "data": { + "noise_psk": "\u0160ifrovac\u00ed kl\u00ed\u010d" + }, + "description": "Pros\u00edm vlo\u017ete \u0161ifrovac\u00ed kl\u00ed\u010d, kter\u00fd jste nastavili ve va\u0161\u00ed konfiguraci pro {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u0160ifrovac\u00ed kl\u00ed\u010d" + }, + "description": "Za\u0159\u00edzen\u00ed ESPHome {name} povolilo transportn\u00ed \u0161ifrov\u00e1n\u00ed nebo zm\u011bnilo \u0161ifrovac\u00ed kl\u00ed\u010d. Pros\u00edm, zadejte aktu\u00e1ln\u00ed kl\u00ed\u010d." + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index cb050046d50ee..737bc5020af76 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -1,27 +1,43 @@ { "config": { "abort": { - "already_configured": "O ESP j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_psk": "A chave de criptografia de transporte \u00e9 inv\u00e1lida. Certifique-se de que corresponde ao que voc\u00ea tem em sua configura\u00e7\u00e3o", "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { "password": "Senha" }, - "description": "Por favor, digite a senha que voc\u00ea definiu em sua configura\u00e7\u00e3o." + "description": "Digite a senha definida na configura\u00e7\u00e3o para {name}." }, "discovery_confirm": { "description": "Voc\u00ea quer adicionar o n\u00f3 ESPHome ` {name} ` ao Home Assistant?", "title": "N\u00f3 ESPHome descoberto" }, + "encryption_key": { + "data": { + "noise_psk": "Chave de encripta\u00e7\u00e3o" + }, + "description": "Insira a chave de criptografia que voc\u00ea definiu em sua configura\u00e7\u00e3o para {name} ." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Chave de encripta\u00e7\u00e3o" + }, + "description": "O dispositivo ESPHome {name} ativou a criptografia de transporte ou alterou a chave de criptografia. Insira a chave atualizada." + }, "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" }, "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/)." diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json new file mode 100644 index 0000000000000..ca12462f388ff --- /dev/null +++ b/homeassistant/components/esphome/translations/sk.json @@ -0,0 +1,46 @@ +{ + "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" + }, + "error": { + "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok \u201eapi:\u201c.", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_psk": "Transportn\u00fd \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd. Pros\u00edm, uistite sa, \u017ee sa zhoduje s t\u00fdm, \u010do m\u00e1te vo svojej konfigur\u00e1cii", + "resolve_error": "Nie je mo\u017en\u00e9 zisti\u0165 adresu ESP. Ak t\u00e1to chyba pretrv\u00e1va, nastavte statick\u00fa IP adresu: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "password": "Heslo" + }, + "description": "Pros\u00edm, zadajte heslo, ktor\u00e9 ste nastavili v konfigur\u00e1cii pre {name}." + }, + "discovery_confirm": { + "description": "Chcete prida\u0165 uzol ESPHome `{name}` do Home Assistant?", + "title": "Objaven\u00fd uzol ESPHome" + }, + "encryption_key": { + "data": { + "noise_psk": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d" + }, + "description": "Pros\u00edm, zadajte \u0161ifrovac\u00ed k\u013e\u00fa\u010d, ktor\u00fd ste nastavili v konfigur\u00e1cii pre {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d" + }, + "description": "Zariadenie ESPHome {name} povolilo transportn\u00e9 \u0161ifrovanie alebo zmenilo \u0161ifrovac\u00ed k\u013e\u00fa\u010d. Pros\u00edm, zadajte aktualizovan\u00fd k\u013e\u00fa\u010d." + }, + "user": { + "data": { + "port": "Port" + }, + "description": "Pros\u00edm, zadajte nastavenia pripojenia v\u00e1\u0161ho uzla [ESPHome](https://esphomelib.com/)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/sv.json b/homeassistant/components/esphome/translations/sv.json index d979ff4821c0e..40fbbf86bc627 100644 --- a/homeassistant/components/esphome/translations/sv.json +++ b/homeassistant/components/esphome/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u00e4r redan konfigurerad" + "already_configured": "ESP \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "connection_error": "Kan inte ansluta till ESP. Se till att din YAML-fil inneh\u00e5ller en 'api:' line.", diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index 7df8bb8d4f3a4..b5435201c23c0 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": ["python-etherscan-api==0.0.3"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyetherscan"] } diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 525283359c9f1..29b0f89cd4ba7 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": ["lakeside==0.12"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["lakeside"] } diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index bbb5e09c446ab..f9a3af200590c 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": ["pyeverlights==0.1.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyeverlights"] } diff --git a/homeassistant/components/evil_genius_labs/translations/cs.json b/homeassistant/components/evil_genius_labs/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/el.json b/homeassistant/components/evil_genius_labs/translations/el.json new file mode 100644 index 0000000000000..7c0975a66f40d --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/pt-BR.json b/homeassistant/components/evil_genius_labs/translations/pt-BR.json new file mode 100644 index 0000000000000..5dda4dc69dc3a --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "timeout": "Tempo limite para estabelecer conex\u00e3o atingido", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 09f9cf81cd163..c2d8f98d40b44 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": ["evohome-async==0.3.15"], "codeowners": ["@zxdavb"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["evohomeasync", "evohomeasync2"] } diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 67c4302e6de80..6680466ecf083 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -10,9 +10,9 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ( - SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT, + SOURCE_INTEGRATION_DISCOVERY, ConfigEntry, ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME @@ -151,7 +151,7 @@ async def async_setup_entry( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ ATTR_SERIAL: camera, CONF_IP_ADDRESS: value["local_ip"], diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index a5a3444c0dd30..780d06383f917 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -259,7 +259,7 @@ async def async_step_user_custom_url(self, user_input=None): step_id="user_custom_url", data_schema=data_schema_custom_url, errors=errors ) - async def async_step_discovery(self, discovery_info): + async def async_step_integration_discovery(self, discovery_info): """Handle a flow for discovered camera without rtsp config entry.""" await self.async_set_unique_id(discovery_info[ATTR_SERIAL]) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 5ce509bfc3c16..211e500cc7d26 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@RenierM26", "@baqs"], "requirements": ["pyezviz==0.2.0.6"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["paho_mqtt", "pyezviz"] } diff --git a/homeassistant/components/ezviz/translations/el.json b/homeassistant/components/ezviz/translations/el.json index 571ba01a0ac16..1b9fd46f1264c 100644 --- a/homeassistant/components/ezviz/translations/el.json +++ b/homeassistant/components/ezviz/translations/el.json @@ -1,21 +1,52 @@ { "config": { "abort": { - "ezviz_cloud_account_missing": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Ezviz cloud \u03bb\u03b5\u03af\u03c0\u03b5\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Ezviz cloud" + "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", + "ezviz_cloud_account_missing": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Ezviz cloud \u03bb\u03b5\u03af\u03c0\u03b5\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Ezviz cloud", + "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", + "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_host": "\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" }, "flow_title": "{serial}", "step": { "confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 RTSP \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 Ezviz {serial} \u03bc\u03b5 IP {ip_address}", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 Ezviz" }, "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Ezviz Cloud" }, "user_custom_url": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03c4\u03b7\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2 \u03c3\u03b1\u03c2", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 Ezviz" } } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u03a4\u03b1 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03b2\u03b9\u03b2\u03ac\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03bf ffmpeg \u03b3\u03b9\u03b1 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2", + "timeout": "\u0391\u03af\u03c4\u03b7\u03bc\u03b1 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/nb.json b/homeassistant/components/ezviz/translations/nb.json new file mode 100644 index 0000000000000..533218a036aec --- /dev/null +++ b/homeassistant/components/ezviz/translations/nb.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + }, + "user_custom_url": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/pt-BR.json b/homeassistant/components/ezviz/translations/pt-BR.json new file mode 100644 index 0000000000000..371686bbf98cd --- /dev/null +++ b/homeassistant/components/ezviz/translations/pt-BR.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "A conta j\u00e1 foi configurada", + "ezviz_cloud_account_missing": "Conta na nuvem Ezviz ausente. Por favor, reconfigure a conta de nuvem Ezviz", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Insira as credenciais RTSP para a c\u00e2mera Ezviz {serial} com IP {ip_address}", + "title": "C\u00e2mera Ezviz descoberta" + }, + "user": { + "data": { + "password": "Senha", + "url": "URL", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se ao Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Senha", + "url": "URL", + "username": "Usu\u00e1rio" + }, + "description": "Especifique manualmente o URL da sua regi\u00e3o", + "title": "Conecte-se ao URL personalizado do Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumentos passados para ffmpeg para c\u00e2meras", + "timeout": "Tempo limite da solicita\u00e7\u00e3o (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/sk.json b/homeassistant/components/ezviz/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/ezviz/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json index caa6c3bb33a9b..d337ce72f862c 100644 --- a/homeassistant/components/faa_delays/manifest.json +++ b/homeassistant/components/faa_delays/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/faa_delays", "requirements": ["faadelays==0.0.7"], "codeowners": ["@ntilley905"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["faadelays"] } diff --git a/homeassistant/components/faa_delays/translations/el.json b/homeassistant/components/faa_delays/translations/el.json index c9eb2c5db3733..245366d355fa3 100644 --- a/homeassistant/components/faa_delays/translations/el.json +++ b/homeassistant/components/faa_delays/translations/el.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03b5\u03c1\u03bf\u03b4\u03c1\u03cc\u03bc\u03b9\u03bf \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_airport": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03b5\u03c1\u03bf\u03b4\u03c1\u03bf\u03bc\u03af\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "id": "\u0391\u03b5\u03c1\u03bf\u03b4\u03c1\u03cc\u03bc\u03b9\u03bf" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b1\u03b5\u03c1\u03bf\u03b4\u03c1\u03bf\u03bc\u03af\u03bf\u03c5 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae IATA", + "title": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03b5\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 FAA" + } } } } \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/pt-BR.json b/homeassistant/components/faa_delays/translations/pt-BR.json new file mode 100644 index 0000000000000..89246ded4a1d7 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_airport": "O c\u00f3digo do aeroporto n\u00e3o \u00e9 v\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "id": "Aeroporto" + }, + "description": "Insira um c\u00f3digo de aeroporto dos EUA no formato IATA", + "title": "Atrasos FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 1063dfda08af0..f74dc7690ca16 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -65,7 +65,7 @@ def __init__(self, name, jail, log_parser): self.last_ban = None self.log_parser = log_parser self.log_parser.ip_regex[self.jail] = re.compile( - fr"\[{re.escape(self.jail)}\]\s*(Ban|Unban) (.*)" + rf"\[{re.escape(self.jail)}\]\s*(Ban|Unban) (.*)" ) _LOGGER.debug("Setting up jail %s", self.jail) diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index ecdafb22b56b3..fbddcb4c0e6b2 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": ["python-family-hub-local==0.0.2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyfamilyhublocal"] } diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index c18e8352b24fc..140fdfe917885 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any from homeassistant.const import ( @@ -112,8 +111,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index 3b7cb4a4f5685..c4edae6f9ee31 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -9,6 +9,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { + "changed_states": "{entity_name} activado o desactivado", "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index b41de6b5657f0..e1c9567dc0f97 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -9,6 +9,7 @@ "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { + "changed_states": "{entity_name} allum\u00e9 ou \u00e9teint", "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} est \u00e9teint", "turned_on": "{entity_name} allum\u00e9" diff --git a/homeassistant/components/fan/translations/id.json b/homeassistant/components/fan/translations/id.json index 054ec10754c1c..c21b90a495a31 100644 --- a/homeassistant/components/fan/translations/id.json +++ b/homeassistant/components/fan/translations/id.json @@ -9,6 +9,8 @@ "is_on": "{entity_name} nyala" }, "trigger_type": { + "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", + "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/fan/translations/nl.json b/homeassistant/components/fan/translations/nl.json index 07f6bbf8c7b8c..aa4474a782f31 100644 --- a/homeassistant/components/fan/translations/nl.json +++ b/homeassistant/components/fan/translations/nl.json @@ -9,6 +9,8 @@ "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { + "changed_states": "{entity_name} in- of uitgeschakeld", + "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" } diff --git a/homeassistant/components/fan/translations/pl.json b/homeassistant/components/fan/translations/pl.json index 8cbf872fb9c07..9e4f6b4534114 100644 --- a/homeassistant/components/fan/translations/pl.json +++ b/homeassistant/components/fan/translations/pl.json @@ -9,6 +9,7 @@ "is_on": "wentylator {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { + "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" diff --git a/homeassistant/components/fan/translations/pt-BR.json b/homeassistant/components/fan/translations/pt-BR.json index f5e9e2f8629fb..97145dd8a60ad 100644 --- a/homeassistant/components/fan/translations/pt-BR.json +++ b/homeassistant/components/fan/translations/pt-BR.json @@ -1,8 +1,18 @@ { "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, "condition_type": { "is_off": "{entity_name} est\u00e1 desligado", "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "changed_states": "{entity_name} ligado ou desligado", + "toggled": "{entity_name} ligado ou desligado", + "turned_off": "{entity_name} for desligado", + "turned_on": "{entity_name} for ligado" } }, "state": { diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index af68bbf29932e..ae953e4271562 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": ["fastdotcom==0.0.3"], "codeowners": ["@rohankapoorcom"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["fastdotcom"] } diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 66874f760ffb2..1a9bb05e140d1 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": ["feedparser==6.0.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["feedparser", "sgmllib3k"] } diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index b3a37cf9e5703..7bc7d5a0e49d0 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.8"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["fiblary3"] } diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 7de047114faaa..b9cdd74baa8b2 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": ["pyfido==2.1.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyfido"] } diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index adfe15b7a3ce3..4e9a2b39d1bea 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -1,5 +1,8 @@ """Support for file notification.""" +from __future__ import annotations + import os +from typing import TextIO import voluptuous as vol @@ -10,7 +13,9 @@ BaseNotificationService, ) from homeassistant.const import CONF_FILENAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" @@ -23,26 +28,33 @@ ) -def get_service(hass, config, discovery_info=None): +def get_service( + hass: HomeAssistant, config: ConfigType, discovery_info=None +) -> FileNotificationService: """Get the file notification service.""" - filename = config[CONF_FILENAME] - timestamp = config[CONF_TIMESTAMP] + filename: str = config[CONF_FILENAME] + timestamp: bool = config[CONF_TIMESTAMP] - return FileNotificationService(hass, filename, timestamp) + return FileNotificationService(filename, timestamp) class FileNotificationService(BaseNotificationService): """Implement the notification service for the File service.""" - def __init__(self, hass, filename, add_timestamp): + def __init__(self, filename: str, add_timestamp: bool) -> None: """Initialize the service.""" - self.filepath = os.path.join(hass.config.config_dir, filename) + self.filename = filename self.add_timestamp = add_timestamp - def send_message(self, message="", **kwargs): + def send_message(self, message="", **kwargs) -> None: """Send a message to a file.""" - with open(self.filepath, "a", encoding="utf8") as file: - if os.stat(self.filepath).st_size == 0: + file: TextIO + if not self.hass.config.config_dir: + return + + filepath: str = os.path.join(self.hass.config.config_dir, self.filename) + with open(filepath, "a", encoding="utf8") as file: + if os.stat(filepath).st_size == 0: title = f"{kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)} notifications (Log started: {dt_util.utcnow().isoformat()})\n{'-' * 80}\n" file.write(title) diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index b4822c7bcd5d5..e69a7701eb993 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant 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 _LOGGER = logging.getLogger(__name__) @@ -41,11 +42,12 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the file sensor.""" - file_path = config[CONF_FILE_PATH] - name = config[CONF_NAME] - unit = config.get(CONF_UNIT_OF_MEASUREMENT) + file_path: str = config[CONF_FILE_PATH] + name: str = config[CONF_NAME] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: + if value_template is not None: value_template.hass = hass if hass.config.is_allowed_path(file_path): @@ -57,33 +59,20 @@ async def async_setup_platform( class FileSensor(SensorEntity): """Implementation of a file sensor.""" - def __init__(self, name, file_path, unit_of_measurement, value_template): + _attr_icon = ICON + + def __init__( + self, + name: str, + file_path: str, + unit_of_measurement: str | None, + value_template: Template | None, + ) -> None: """Initialize the file sensor.""" - self._name = name + self._attr_name = name self._file_path = file_path - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._val_tpl = value_template - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state def update(self): """Get the latest entry from a file and updates the state.""" @@ -100,8 +89,8 @@ def update(self): return if self._val_tpl is not None: - self._state = self._val_tpl.async_render_with_possible_json_value( - data, None + self._attr_native_value = ( + self._val_tpl.async_render_with_possible_json_value(data, None) ) else: - self._state = data + self._attr_native_value = data diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index e409b346c6d7b..56542a0aadd57 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -4,10 +4,14 @@ import datetime import logging import os +import pathlib import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import DATA_MEGABYTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -23,7 +27,7 @@ CONF_FILE_PATHS = "file_paths" ICON = "mdi:file" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} ) @@ -39,11 +43,23 @@ def setup_platform( setup_reload_service(hass, DOMAIN, PLATFORMS) sensors = [] + paths = set() for path in config[CONF_FILE_PATHS]: + try: + fullpath = str(pathlib.Path(path).absolute()) + except OSError as error: + _LOGGER.error("Can not access file %s, error %s", path, error) + continue + + if fullpath in paths: + continue + paths.add(fullpath) + if not hass.config.is_allowed_path(path): _LOGGER.error("Filepath %s is not valid or allowed", path) continue - sensors.append(Filesize(path)) + + sensors.append(Filesize(fullpath)) if sensors: add_entities(sensors, True) @@ -52,48 +68,28 @@ def setup_platform( class Filesize(SensorEntity): """Encapsulates file size information.""" - def __init__(self, path): + _attr_native_unit_of_measurement = DATA_MEGABYTES + _attr_icon = ICON + + def __init__(self, path: str) -> None: """Initialize the data object.""" self._path = path # Need to check its a valid path - self._size = None - self._last_updated = None - self._name = path.split("/")[-1] - self._unit_of_measurement = DATA_MEGABYTES + self._attr_name = path.split("/")[-1] - def update(self): + def update(self) -> None: """Update the sensor.""" - statinfo = os.stat(self._path) - self._size = statinfo.st_size - last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime) - self._last_updated = last_updated.isoformat() - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the size of the file in MB.""" - decimals = 2 - state_mb = round(self._size / 1e6, decimals) - return state_mb - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def extra_state_attributes(self): - """Return other details about the sensor state.""" - return { + try: + statinfo = os.stat(self._path) + except OSError as error: + _LOGGER.error("Can not retrieve file statistics %s", error) + self._attr_native_value = None + return + + size = statinfo.st_size + last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime).isoformat() + self._attr_native_value = round(size / 1e6, 2) if size else None + self._attr_extra_state_attributes = { "path": self._path, - "last_updated": self._last_updated, - "bytes": self._size, + "last_updated": last_updated, + "bytes": size, } - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 8a75615e617e7..a5b54a621a771 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -3,7 +3,7 @@ from collections import Counter, deque from copy import copy -from datetime import timedelta +from datetime import datetime, timedelta from functools import partial import logging from numbers import Number @@ -19,6 +19,7 @@ DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, ) from homeassistant.const import ( @@ -28,6 +29,7 @@ ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, CONF_NAME, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -50,7 +52,7 @@ FILTER_NAME_THROTTLE = "throttle" FILTER_NAME_TIME_THROTTLE = "time_throttle" FILTER_NAME_TIME_SMA = "time_simple_moving_average" -FILTERS = Registry() +FILTERS: Registry[str, type[Filter]] = Registry() CONF_FILTERS = "filters" CONF_FILTER_NAME = "filter" @@ -149,6 +151,7 @@ cv.entity_domain(INPUT_NUMBER_DOMAIN), ), vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_FILTERS): vol.All( cv.ensure_list, [ @@ -177,6 +180,7 @@ async def async_setup_platform( await async_setup_reload_service(hass, DOMAIN, PLATFORMS) name = config.get(CONF_NAME) + unique_id = config.get(CONF_UNIQUE_ID) entity_id = config.get(CONF_ENTITY_ID) filters = [ @@ -184,15 +188,16 @@ async def async_setup_platform( for _filter in config[CONF_FILTERS] ] - async_add_entities([SensorFilter(name, entity_id, filters)]) + async_add_entities([SensorFilter(name, unique_id, entity_id, filters)]) class SensorFilter(SensorEntity): """Representation of a Filter Sensor.""" - def __init__(self, name, entity_id, filters): + def __init__(self, name, unique_id, entity_id, filters): """Initialize the sensor.""" self._name = name + self._attr_unique_id = unique_id self._entity = entity_id self._unit_of_measurement = None self._state = None @@ -346,6 +351,9 @@ def name(self): @property def native_value(self): """Return the state of the sensor.""" + if self._device_class == SensorDeviceClass.TIMESTAMP: + return datetime.fromisoformat(self._state) + return self._state @property diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index 854f3a2f195f1..ede1025a6db0c 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": ["fints==1.0.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["fints", "mt_940", "sepaxml"] } diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 4cdb7ec0c42fd..ffd8230794065 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -1,4 +1,6 @@ """The FireServiceRota integration.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -195,25 +197,25 @@ async def update_call(self, func, *args): return await self._hass.async_add_executor_job(func, *args) - async def async_update(self) -> object: + async def async_update(self) -> dict | None: """Get the latest availability data.""" data = await self.update_call( self.fsr.get_availability, str(self._hass.config.time_zone) ) if not data: - return + return None self.on_duty = bool(data.get("available")) _LOGGER.debug("Updated availability data: %s", data) return data - async def async_response_update(self) -> object: + async def async_response_update(self) -> dict | None: """Get the latest incident response data.""" if not self.incident_id: - return + return None _LOGGER.debug("Updating response data for incident id %s", self.incident_id) diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index c175e58cae876..9e4d5b123f5a6 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -1,4 +1,8 @@ """Binary Sensor platform for FireServiceRota integration.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -8,6 +12,7 @@ DataUpdateCoordinator, ) +from . import FireServiceRotaClient from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN @@ -16,7 +21,9 @@ async def async_setup_entry( ) -> None: """Set up FireServiceRota binary sensor based on a config entry.""" - client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] + client: FireServiceRotaClient = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][ + DATA_CLIENT + ] coordinator: DataUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][ entry.entry_id @@ -28,13 +35,18 @@ async def async_setup_entry( class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of an FireServiceRota sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, client, entry): + def __init__( + self, + coordinator: DataUpdateCoordinator, + client: FireServiceRotaClient, + entry: ConfigEntry, + ) -> None: """Initialize.""" super().__init__(coordinator) self._client = client self._unique_id = f"{entry.unique_id}_Duty" - self._state = None + self._state: bool | None = None @property def name(self) -> str: @@ -55,7 +67,7 @@ def unique_id(self) -> str: return self._unique_id @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the binary sensor.""" self._state = self._client.on_duty @@ -63,9 +75,9 @@ def is_on(self) -> bool: return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return available attributes for binary sensor.""" - attr = {} + attr: dict[str, Any] = {} if not self.coordinator.data: return attr diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index 1eea9fbfbf188..317f72dbae98a 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/fireservicerota", "requirements": ["pyfireservicerota==0.0.43"], "codeowners": ["@cyberjunky"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyfireservicerota"] } diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index a17892a859181..66878b731452e 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -1,5 +1,6 @@ """Sensor platform for FireServiceRota integration.""" import logging +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -65,9 +66,9 @@ def should_poll(self) -> bool: return False @property - def extra_state_attributes(self) -> object: + def extra_state_attributes(self) -> dict[str, Any]: """Return available attributes for sensor.""" - attr = {} + attr: dict[str, Any] = {} if not (data := self._state_attributes): return attr diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 7202420e571a4..e625ac5deb55b 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -1,5 +1,6 @@ """Switch platform for FireServiceRota integration.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -73,9 +74,9 @@ def available(self): return self._client.on_duty @property - def extra_state_attributes(self) -> object: + def extra_state_attributes(self) -> dict[str, Any]: """Return available attributes for switch.""" - attr = {} + attr: dict[str, Any] = {} if not self._state_attributes: return attr @@ -140,10 +141,11 @@ async def async_update(self) -> bool: data = await self._client.async_response_update() if not data or "status" not in data: - return + return False self._state = data["status"] == "acknowledged" self._state_attributes = data self._state_icon = data["status"] _LOGGER.debug("Set state of entity 'Response Switch' to '%s'", self._state) + return True diff --git a/homeassistant/components/fireservicerota/translations/el.json b/homeassistant/components/fireservicerota/translations/el.json index bf11ee5dd0017..467a98a49ad41 100644 --- a/homeassistant/components/fireservicerota/translations/el.json +++ b/homeassistant/components/fireservicerota/translations/el.json @@ -1,9 +1,27 @@ { "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", + "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" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "error": { + "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": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ad\u03b3\u03b9\u03bd\u03b1\u03bd \u03ac\u03ba\u03c5\u03c1\u03b1, \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + }, "user": { "data": { - "url": "\u0399\u03c3\u03c4\u03bf\u03c3\u03b5\u03bb\u03af\u03b4\u03b1" + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0399\u03c3\u03c4\u03bf\u03c3\u03b5\u03bb\u03af\u03b4\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } } diff --git a/homeassistant/components/fireservicerota/translations/pt-BR.json b/homeassistant/components/fireservicerota/translations/pt-BR.json new file mode 100644 index 0000000000000..45b55aa532447 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth": { + "data": { + "password": "Senha" + }, + "description": "Os tokens de autentica\u00e7\u00e3o se tornaram inv\u00e1lidos, fa\u00e7a login para recri\u00e1-los." + }, + "user": { + "data": { + "password": "Senha", + "url": "Site", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sk.json b/homeassistant/components/fireservicerota/translations/sk.json new file mode 100644 index 0000000000000..879d148fd130b --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index cef9db620b87b..ffc4d39a89b0f 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -190,7 +190,7 @@ async def handle_shutdown(event) -> None: device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={}, + connections=set(), identifiers={(DOMAIN, board.name)}, manufacturer=FIRMATA_MANUFACTURER, name=board.name, diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index 1f0052732ea68..002775edb0ce1 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -1,6 +1,9 @@ """Code to handle a Firmata board.""" +from __future__ import annotations + +from collections.abc import Mapping import logging -from typing import Union +from typing import Literal, Union from pymata_express.pymata_express import PymataExpress from pymata_express.pymata_express_serial import serial @@ -32,18 +35,18 @@ class FirmataBoard: """Manages a single Firmata board.""" - def __init__(self, config: dict) -> None: + def __init__(self, config: Mapping) -> None: """Initialize the board.""" self.config = config - self.api = None - self.firmware_version = None + self.api: PymataExpress = None + self.firmware_version: str | None = None self.protocol_version = None self.name = self.config[CONF_NAME] self.switches = [] self.lights = [] self.binary_sensors = [] self.sensors = [] - self.used_pins = [] + self.used_pins: list[FirmataPinType] = [] if CONF_SWITCHES in self.config: self.switches = self.config[CONF_SWITCHES] @@ -118,8 +121,10 @@ def mark_pin_used(self, pin: FirmataPinType) -> bool: self.used_pins.append(pin) return True - def get_pin_type(self, pin: FirmataPinType) -> tuple: + def get_pin_type(self, pin: FirmataPinType) -> tuple[Literal[0, 1], int]: """Return the type and Firmata location of a pin on the board.""" + pin_type: Literal[0, 1] + firmata_pin: int if isinstance(pin, str): pin_type = PIN_TYPE_ANALOG firmata_pin = int(pin[1:]) @@ -130,7 +135,7 @@ def get_pin_type(self, pin: FirmataPinType) -> tuple: return (pin_type, firmata_pin) -async def get_board(data: dict) -> PymataExpress: +async def get_board(data: Mapping) -> PymataExpress: """Create a Pymata board object.""" board_data = {} diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py index 091d724229c74..da722b51897ab 100644 --- a/homeassistant/components/firmata/const.py +++ b/homeassistant/components/firmata/const.py @@ -1,4 +1,6 @@ """Constants for the Firmata component.""" +from typing import Final + from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_LIGHTS, @@ -19,8 +21,8 @@ PIN_MODE_PWM = "PWM" PIN_MODE_INPUT = "INPUT" PIN_MODE_PULLUP = "PULLUP" -PIN_TYPE_ANALOG = 1 -PIN_TYPE_DIGITAL = 0 +PIN_TYPE_ANALOG: Final = 1 +PIN_TYPE_DIGITAL: Final = 0 CONF_SAMPLING_INTERVAL = "sampling_interval" CONF_SERIAL_BAUD_RATE = "serial_baud_rate" CONF_SERIAL_PORT = "serial_port" diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 0f248e0b9d77b..0e66656421b82 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -20,7 +20,7 @@ def __init__(self, api): def device_info(self) -> DeviceInfo: """Return device info.""" return DeviceInfo( - connections={}, + connections=set(), identifiers={(DOMAIN, self._api.board.name)}, manufacturer=FIRMATA_MANUFACTURER, name=self._api.board.name, @@ -33,7 +33,7 @@ class FirmataPinEntity(FirmataEntity): def __init__( self, - api: type[FirmataBoardPin], + api: FirmataBoardPin, config_entry: ConfigEntry, name: str, pin: FirmataPinType, diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index d42e72f992aaa..de51105811465 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -58,7 +58,7 @@ class FirmataLight(FirmataPinEntity, LightEntity): def __init__( self, - api: type[FirmataBoardPin], + api: FirmataBoardPin, config_entry: ConfigEntry, name: str, pin: FirmataPinType, diff --git a/homeassistant/components/firmata/manifest.json b/homeassistant/components/firmata/manifest.json index 7af4624669bb0..ccfce906047af 100644 --- a/homeassistant/components/firmata/manifest.json +++ b/homeassistant/components/firmata/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/firmata", "requirements": ["pymata-express==1.19"], "codeowners": ["@DaAwesomeP"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pymata_express"] } diff --git a/homeassistant/components/firmata/pin.py b/homeassistant/components/firmata/pin.py index 6dadb07fd6313..190889914b3b2 100644 --- a/homeassistant/components/firmata/pin.py +++ b/homeassistant/components/firmata/pin.py @@ -3,6 +3,7 @@ from collections.abc import Callable import logging +from typing import cast from .board import FirmataBoard, FirmataPinType from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP, PIN_TYPE_ANALOG @@ -23,11 +24,12 @@ def __init__(self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str) -> N self._pin = pin self._pin_mode = pin_mode self._pin_type, self._firmata_pin = self.board.get_pin_type(self._pin) - self._state = None + self._state: bool | int | None = None + self._analog_pin: int | None = None if self._pin_type == PIN_TYPE_ANALOG: # Pymata wants the analog pin formatted as the # from "A#" - self._analog_pin = int(self._pin[1:]) + self._analog_pin = int(cast(str, self._pin)[1:]) def setup(self): """Set up a pin and make sure it is valid.""" @@ -38,6 +40,8 @@ def setup(self): class FirmataBinaryDigitalOutput(FirmataBoardPin): """Representation of a Firmata Digital Output Pin.""" + _state: bool + def __init__( self, board: FirmataBoard, @@ -92,6 +96,8 @@ async def turn_off(self) -> None: class FirmataPWMOutput(FirmataBoardPin): """Representation of a Firmata PWM/analog Output Pin.""" + _state: int + def __init__( self, board: FirmataBoard, @@ -139,12 +145,14 @@ async def set_level(self, level: int) -> None: class FirmataBinaryDigitalInput(FirmataBoardPin): """Representation of a Firmata Digital Input Pin.""" + _state: bool + def __init__( self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, negate: bool ) -> None: """Initialize the digital input pin.""" self._negate = negate - self._forward_callback = None + self._forward_callback: Callable[[], None] super().__init__(board, pin, pin_mode) async def start_pin(self, forward_callback: Callable[[], None]) -> None: @@ -206,12 +214,15 @@ async def latch_callback(self, data: list) -> None: class FirmataAnalogInput(FirmataBoardPin): """Representation of a Firmata Analog Input Pin.""" + _analog_pin: int + _state: int + def __init__( self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, differential: int ) -> None: """Initialize the analog input pin.""" self._differential = differential - self._forward_callback = None + self._forward_callback: Callable[[], None] super().__init__(board, pin, pin_mode) async def start_pin(self, forward_callback: Callable[[], None]) -> None: diff --git a/homeassistant/components/firmata/translations/el.json b/homeassistant/components/firmata/translations/el.json new file mode 100644 index 0000000000000..eb707f48797b1 --- /dev/null +++ b/homeassistant/components/firmata/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/pt-BR.json b/homeassistant/components/firmata/translations/pt-BR.json new file mode 100644 index 0000000000000..fa50f0901aae9 --- /dev/null +++ b/homeassistant/components/firmata/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha ao conectar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index b848a344f1f3c..39bfa2c8e37f7 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -5,5 +5,6 @@ "requirements": ["fitbit==0.3.1"], "dependencies": ["configurator", "http"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["fitbit"] } diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py new file mode 100644 index 0000000000000..2004aacd165b2 --- /dev/null +++ b/homeassistant/components/fivem/__init__.py @@ -0,0 +1,165 @@ +"""The FiveM integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any + +from fivem import FiveM, FiveMServerOfflineError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTR_PLAYERS_LIST, + ATTR_RESOURCES_LIST, + DOMAIN, + MANUFACTURER, + NAME_PLAYERS_MAX, + NAME_PLAYERS_ONLINE, + NAME_RESOURCES, + NAME_STATUS, + SCAN_INTERVAL, +) + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up FiveM from a config entry.""" + _LOGGER.debug( + "Create FiveM server instance for '%s:%s'", + entry.data[CONF_HOST], + entry.data[CONF_PORT], + ) + + coordinator = FiveMDataUpdateCoordinator(hass, entry.data, entry.entry_id) + + try: + await coordinator.initialize() + except FiveMServerOfflineError as err: + raise ConfigEntryNotReady from err + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(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 + + +class FiveMDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching FiveM data.""" + + def __init__( + self, hass: HomeAssistant, config_data: Mapping[str, Any], unique_id: str + ) -> None: + """Initialize server instance.""" + self.unique_id = unique_id + self.server = None + self.version = None + self.game_name: str | None = None + + self.host = config_data[CONF_HOST] + + self._fivem = FiveM(self.host, config_data[CONF_PORT]) + + update_interval = timedelta(seconds=SCAN_INTERVAL) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def initialize(self) -> None: + """Initialize the FiveM server.""" + info = await self._fivem.get_info_raw() + self.server = info["server"] + self.version = info["version"] + self.game_name = info["vars"]["gamename"] + + async def _async_update_data(self) -> dict[str, Any]: + """Get server data from 3rd party library and update properties.""" + try: + server = await self._fivem.get_server() + except FiveMServerOfflineError as err: + raise UpdateFailed from err + + players_list: list[str] = [] + for player in server.players: + players_list.append(player.name) + players_list.sort() + + resources_list = server.resources + resources_list.sort() + + return { + NAME_PLAYERS_ONLINE: len(players_list), + NAME_PLAYERS_MAX: server.max_players, + NAME_RESOURCES: len(resources_list), + NAME_STATUS: self.last_update_success, + ATTR_PLAYERS_LIST: players_list, + ATTR_RESOURCES_LIST: resources_list, + } + + +@dataclass +class FiveMEntityDescription(EntityDescription): + """Describes FiveM entity.""" + + extra_attrs: list[str] | None = None + + +class FiveMEntity(CoordinatorEntity): + """Representation of a FiveM base entity.""" + + coordinator: FiveMDataUpdateCoordinator + entity_description: FiveMEntityDescription + + def __init__( + self, + coordinator: FiveMDataUpdateCoordinator, + description: FiveMEntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = description + + self._attr_name = f"{self.coordinator.host} {description.name}" + self._attr_unique_id = f"{self.coordinator.unique_id}-{description.key}".lower() + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.coordinator.unique_id)}, + manufacturer=MANUFACTURER, + model=self.coordinator.server, + name=self.coordinator.host, + sw_version=self.coordinator.version, + ) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the extra attributes of the sensor.""" + if self.entity_description.extra_attrs is None: + return None + + return { + attr: self.coordinator.data[attr] + for attr in self.entity_description.extra_attrs + } diff --git a/homeassistant/components/fivem/binary_sensor.py b/homeassistant/components/fivem/binary_sensor.py new file mode 100644 index 0000000000000..f3f253fe530e4 --- /dev/null +++ b/homeassistant/components/fivem/binary_sensor.py @@ -0,0 +1,54 @@ +"""The FiveM binary sensor platform.""" +from dataclasses import dataclass + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FiveMEntity, FiveMEntityDescription +from .const import DOMAIN, NAME_STATUS + + +@dataclass +class FiveMBinarySensorEntityDescription( + BinarySensorEntityDescription, FiveMEntityDescription +): + """Describes FiveM binary sensor entity.""" + + +BINARY_SENSORS: tuple[FiveMBinarySensorEntityDescription, ...] = ( + FiveMBinarySensorEntityDescription( + key=NAME_STATUS, + name=NAME_STATUS, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the FiveM binary sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [FiveMSensorEntity(coordinator, description) for description in BINARY_SENSORS] + ) + + +class FiveMSensorEntity(FiveMEntity, BinarySensorEntity): + """Representation of a FiveM sensor base entity.""" + + entity_description: FiveMBinarySensorEntityDescription + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/fivem/config_flow.py b/homeassistant/components/fivem/config_flow.py new file mode 100644 index 0000000000000..e564faa81b716 --- /dev/null +++ b/homeassistant/components/fivem/config_flow.py @@ -0,0 +1,75 @@ +"""Config flow for FiveM integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from fivem import FiveM, FiveMServerOfflineError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 30120 + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) + + +async def validate_input(data: dict[str, Any]) -> None: + """Validate the user input allows us to connect.""" + + fivem = FiveM(data[CONF_HOST], data[CONF_PORT]) + info = await fivem.get_info_raw() + + game_name = info.get("vars")["gamename"] + if game_name is None or game_name != "gta5": + raise InvalidGameNameError + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for FiveM.""" + + 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 + ) + + errors = {} + + try: + await validate_input(user_input) + except FiveMServerOfflineError: + errors["base"] = "cannot_connect" + except InvalidGameNameError: + errors["base"] = "invalid_game_name" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self._async_abort_entries_match(user_input) + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class InvalidGameNameError(Exception): + """Handle errors in the game name from the api.""" diff --git a/homeassistant/components/fivem/const.py b/homeassistant/components/fivem/const.py new file mode 100644 index 0000000000000..1676dc9f2b371 --- /dev/null +++ b/homeassistant/components/fivem/const.py @@ -0,0 +1,23 @@ +"""Constants for the FiveM integration.""" + +ATTR_PLAYERS_LIST = "players_list" +ATTR_RESOURCES_LIST = "resources_list" + +DOMAIN = "fivem" + +ICON_PLAYERS_MAX = "mdi:account-multiple" +ICON_PLAYERS_ONLINE = "mdi:account-multiple" +ICON_RESOURCES = "mdi:playlist-check" + +MANUFACTURER = "Cfx.re" + +NAME_PLAYERS_MAX = "Players Max" +NAME_PLAYERS_ONLINE = "Players Online" +NAME_RESOURCES = "Resources" +NAME_STATUS = "Status" + +SCAN_INTERVAL = 60 + +UNIT_PLAYERS_MAX = "players" +UNIT_PLAYERS_ONLINE = "players" +UNIT_RESOURCES = "resources" diff --git a/homeassistant/components/fivem/manifest.json b/homeassistant/components/fivem/manifest.json new file mode 100644 index 0000000000000..4a18df0fc951e --- /dev/null +++ b/homeassistant/components/fivem/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "fivem", + "name": "FiveM", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/fivem", + "requirements": [ + "fivem-api==0.1.2" + ], + "codeowners": [ + "@Sander0542" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/fivem/sensor.py b/homeassistant/components/fivem/sensor.py new file mode 100644 index 0000000000000..31e23565a6f6a --- /dev/null +++ b/homeassistant/components/fivem/sensor.py @@ -0,0 +1,78 @@ +"""The FiveM sensor platform.""" +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FiveMEntity, FiveMEntityDescription +from .const import ( + ATTR_PLAYERS_LIST, + ATTR_RESOURCES_LIST, + DOMAIN, + ICON_PLAYERS_MAX, + ICON_PLAYERS_ONLINE, + ICON_RESOURCES, + NAME_PLAYERS_MAX, + NAME_PLAYERS_ONLINE, + NAME_RESOURCES, + UNIT_PLAYERS_MAX, + UNIT_PLAYERS_ONLINE, + UNIT_RESOURCES, +) + + +@dataclass +class FiveMSensorEntityDescription(SensorEntityDescription, FiveMEntityDescription): + """Describes FiveM sensor entity.""" + + +SENSORS: tuple[FiveMSensorEntityDescription, ...] = ( + FiveMSensorEntityDescription( + key=NAME_PLAYERS_MAX, + name=NAME_PLAYERS_MAX, + icon=ICON_PLAYERS_MAX, + native_unit_of_measurement=UNIT_PLAYERS_MAX, + ), + FiveMSensorEntityDescription( + key=NAME_PLAYERS_ONLINE, + name=NAME_PLAYERS_ONLINE, + icon=ICON_PLAYERS_ONLINE, + native_unit_of_measurement=UNIT_PLAYERS_ONLINE, + extra_attrs=[ATTR_PLAYERS_LIST], + ), + FiveMSensorEntityDescription( + key=NAME_RESOURCES, + name=NAME_RESOURCES, + icon=ICON_RESOURCES, + native_unit_of_measurement=UNIT_RESOURCES, + extra_attrs=[ATTR_RESOURCES_LIST], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the FiveM sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + # Add sensor entities. + async_add_entities( + [FiveMSensorEntity(coordinator, description) for description in SENSORS] + ) + + +class FiveMSensorEntity(FiveMEntity, SensorEntity): + """Representation of a FiveM sensor base entity.""" + + entity_description: FiveMSensorEntityDescription + + @property + def native_value(self) -> Any: + """Return the state of the sensor.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/fivem/strings.json b/homeassistant/components/fivem/strings.json new file mode 100644 index 0000000000000..03afcc11f27b5 --- /dev/null +++ b/homeassistant/components/fivem/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "Failed to connect. Please check the host and port and try again. Also ensure that you are running the latest FiveM server.", + "invalid_game_name": "The api of the game you are trying to connect to is not a FiveM game.", + "unknown_error": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/bg.json b/homeassistant/components/fivem/translations/bg.json new file mode 100644 index 0000000000000..98f8a2a7d266f --- /dev/null +++ b/homeassistant/components/fivem/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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": { + "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/ca.json b/homeassistant/components/fivem/translations/ca.json new file mode 100644 index 0000000000000..8e2a192a18c02 --- /dev/null +++ b/homeassistant/components/fivem/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que est\u00e0s utilitzant la versi\u00f3 del servidor FiveM m\u00e9s recent.", + "invalid_game_name": "L'API del joc al qual est\u00e0s intentant connectar-te no \u00e9s d'un joc FiveM.", + "invalid_gamename": "L'API del joc al qual est\u00e0s intentant connectar-te no \u00e9s d'un joc FiveM.", + "unknown_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/cs.json b/homeassistant/components/fivem/translations/cs.json new file mode 100644 index 0000000000000..2455bf8695da8 --- /dev/null +++ b/homeassistant/components/fivem/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/de.json b/homeassistant/components/fivem/translations/de.json new file mode 100644 index 0000000000000..5c6852a126ee1 --- /dev/null +++ b/homeassistant/components/fivem/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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.", + "invalid_gamename": "Die API des Spiels, mit dem du dich verbinden willst, ist kein FiveM-Spiel.", + "unknown_error": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/el.json b/homeassistant/components/fivem/translations/el.json new file mode 100644 index 0000000000000..29e798361fffb --- /dev/null +++ b/homeassistant/components/fivem/translations/el.json @@ -0,0 +1,22 @@ +{ + "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\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae FiveM.", + "invalid_game_name": "\u03a4\u03bf api \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 FiveM.", + "invalid_gamename": "\u03a4\u03bf api \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 FiveM.", + "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "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", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/en.json b/homeassistant/components/fivem/translations/en.json new file mode 100644 index 0000000000000..e07c0666e2461 --- /dev/null +++ b/homeassistant/components/fivem/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect. Please check the host and port and try again. Also ensure that you are running the latest FiveM server.", + "invalid_game_name": "The api of the game you are trying to connect to is not a FiveM game.", + "invalid_gamename": "The api of the game you are trying to connect to is not a FiveM game.", + "unknown_error": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json new file mode 100644 index 0000000000000..d8b3f3c6e1cb7 --- /dev/null +++ b/homeassistant/components/fivem/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Error al conectarse. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Aseg\u00farese tambi\u00e9n de que est\u00e1 ejecutando el servidor FiveM m\u00e1s reciente.", + "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", + "invalid_gamename": "La API del juego al que intentas conectarte no es un juego de FiveM.", + "unknown_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Anfitri\u00f3n", + "name": "Nombre", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/et.json b/homeassistant/components/fivem/translations/et.json new file mode 100644 index 0000000000000..0ca491bd2322b --- /dev/null +++ b/homeassistant/components/fivem/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine eba\u00f5nnestus. Kontrolli hosti ja porti ning proovi uuesti. Veendu, et kasutad uusimat FiveM-i serverit.", + "invalid_game_name": "M\u00e4ngu API, millega proovid \u00fchendust luua, ei ole FiveM-m\u00e4ng.", + "invalid_gamename": "M\u00e4ngu API, millega proovid \u00fchendust luua, ei ole FiveM-m\u00e4ng.", + "unknown_error": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nimi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/fr.json b/homeassistant/components/fivem/translations/fr.json new file mode 100644 index 0000000000000..4cd16be65a36d --- /dev/null +++ b/homeassistant/components/fivem/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion. Veuillez v\u00e9rifier l'h\u00f4te et le port et r\u00e9essayer. Assurez-vous \u00e9galement que vous utilisez le dernier serveur FiveM.", + "invalid_game_name": "L'API du jeu auquel vous essayez de vous connecter n'est pas un jeu FiveM.", + "invalid_gamename": "L\u2019API du jeu auquel vous essayez de vous connecter n\u2019est pas un jeu FiveM.", + "unknown_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/he.json b/homeassistant/components/fivem/translations/he.json new file mode 100644 index 0000000000000..3784dae202df9 --- /dev/null +++ b/homeassistant/components/fivem/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": { + "unknown_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json new file mode 100644 index 0000000000000..b307c6e79fcf3 --- /dev/null +++ b/homeassistant/components/fivem/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", + "invalid_game_name": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", + "invalid_gamename": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "name": "N\u00e9v", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/id.json b/homeassistant/components/fivem/translations/id.json new file mode 100644 index 0000000000000..3cf44f86f5d8a --- /dev/null +++ b/homeassistant/components/fivem/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung ke server. Periksa host dan port lalu coba lagi. Pastikan juga Anda menjalankan server FiveM terbaru.", + "invalid_game_name": "API dari permainan yang Anda coba hubungkan bukanlah game FiveM.", + "invalid_gamename": "API dari permainan yang Anda coba hubungkan bukanlah game FiveM.", + "unknown_error": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/it.json b/homeassistant/components/fivem/translations/it.json new file mode 100644 index 0000000000000..0128d1fbecaf5 --- /dev/null +++ b/homeassistant/components/fivem/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Connessione non riuscita. Controlla l'host e la porta e riprova. Assicurati inoltre di eseguire il server FiveM pi\u00f9 recente.", + "invalid_game_name": "L'API del gioco a cui stai tentando di connetterti non \u00e8 un gioco FiveM.", + "invalid_gamename": "L'API del gioco a cui stai tentando di connetterti non \u00e8 un gioco FiveM.", + "unknown_error": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/ja.json b/homeassistant/components/fivem/translations/ja.json new file mode 100644 index 0000000000000..eb398cccff462 --- /dev/null +++ b/homeassistant/components/fivem/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "FiveM\u30b5\u30fc\u30d0\u30fc\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u6700\u65b0\u306eFiveM\u30b5\u30fc\u30d0\u30fc\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_game_name": "\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u30b2\u30fc\u30e0\u306eAPI\u306f\u3001FiveM\u306e\u30b2\u30fc\u30e0\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "invalid_gamename": "\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u30b2\u30fc\u30e0\u306eAPI\u306f\u3001FiveM\u306e\u30b2\u30fc\u30e0\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/nl.json b/homeassistant/components/fivem/translations/nl.json new file mode 100644 index 0000000000000..599bcbc771e78 --- /dev/null +++ b/homeassistant/components/fivem/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken. Controleer de host en poort en probeer het opnieuw. Zorg er ook voor dat u de nieuwste FiveM-server gebruikt.", + "invalid_game_name": "De api van het spel waarmee je probeert te verbinden is geen FiveM spel.", + "invalid_gamename": "De api van het spel waarmee je probeert te verbinden is geen FiveM spel.", + "unknown_error": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/no.json b/homeassistant/components/fivem/translations/no.json new file mode 100644 index 0000000000000..ac292c10b643b --- /dev/null +++ b/homeassistant/components/fivem/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes. Kontroller verten og porten og pr\u00f8v igjen. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer den nyeste FiveM-serveren.", + "invalid_game_name": "API-et til spillet du pr\u00f8ver \u00e5 koble til er ikke et FiveM-spill.", + "invalid_gamename": "API-et til spillet du pr\u00f8ver \u00e5 koble til er ikke et FiveM-spill.", + "unknown_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/pl.json b/homeassistant/components/fivem/translations/pl.json new file mode 100644 index 0000000000000..420ebca7463e5 --- /dev/null +++ b/homeassistant/components/fivem/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a adres hosta oraz port i spr\u00f3buj ponownie. Upewnij si\u0119, \u017ce posiadasz najnowsz\u0105 wersj\u0119 serwera FiveM.", + "invalid_game_name": "API gry, do kt\u00f3rej pr\u00f3bujesz si\u0119 po\u0142\u0105czy\u0107, nie jest gr\u0105 FiveM.", + "invalid_gamename": "API gry, do kt\u00f3rej pr\u00f3bujesz si\u0119 po\u0142\u0105czy\u0107, nie jest gr\u0105 FiveM.", + "unknown_error": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/pt-BR.json b/homeassistant/components/fivem/translations/pt-BR.json new file mode 100644 index 0000000000000..b576192718fbf --- /dev/null +++ b/homeassistant/components/fivem/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao se conectar. Verifique o host e a porta e tente novamente. Verifique tamb\u00e9m se voc\u00ea est\u00e1 executando o servidor FiveM mais recente.", + "invalid_game_name": "A API do jogo ao qual voc\u00ea est\u00e1 tentando se conectar n\u00e3o \u00e9 um jogo FiveM.", + "invalid_gamename": "A API do jogo ao qual voc\u00ea est\u00e1 tentando se conectar n\u00e3o \u00e9 um jogo FiveM.", + "unknown_error": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/ru.json b/homeassistant/components/fivem/translations/ru.json new file mode 100644 index 0000000000000..c6da81663cae2 --- /dev/null +++ b/homeassistant/components/fivem/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044e\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 FiveM.", + "invalid_game_name": "API \u0438\u0433\u0440\u044b, \u043a \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0433\u0440\u043e\u0439 FiveM.", + "invalid_gamename": "API \u0438\u0433\u0440\u044b, \u043a \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0433\u0440\u043e\u0439 FiveM.", + "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/sk.json b/homeassistant/components/fivem/translations/sk.json new file mode 100644 index 0000000000000..39d2e182c40be --- /dev/null +++ b/homeassistant/components/fivem/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/tr.json b/homeassistant/components/fivem/translations/tr.json new file mode 100644 index 0000000000000..46921dd33c01f --- /dev/null +++ b/homeassistant/components/fivem/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131. L\u00fctfen ana bilgisayar\u0131 ve ba\u011flant\u0131 noktas\u0131n\u0131 kontrol edin ve tekrar deneyin. Ayr\u0131ca en son FiveM sunucusunu \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131n\u0131zdan emin olun.", + "invalid_game_name": "Ba\u011flanmaya \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131z oyunun api'si bir FiveM oyunu de\u011fil.", + "invalid_gamename": "Ba\u011flanmaya \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131z oyunun api'si bir FiveM oyunu de\u011fil.", + "unknown_error": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "name": "Ad", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/uk.json b/homeassistant/components/fivem/translations/uk.json new file mode 100644 index 0000000000000..b932679af93b1 --- /dev/null +++ b/homeassistant/components/fivem/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/zh-Hant.json b/homeassistant/components/fivem/translations/zh-Hant.json new file mode 100644 index 0000000000000..3b1527f7a35db --- /dev/null +++ b/homeassistant/components/fivem/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u57f7\u884c\u6700\u65b0\u7248 FiveM \u4f3a\u670d\u5668\u3002", + "invalid_game_name": "\u5617\u8a66\u9023\u7dda\u7684\u904a\u6232 API \u4e26\u975e FiveM \u904a\u6232\u3002", + "invalid_gamename": "\u5617\u8a66\u9023\u7dda\u7684\u904a\u6232 API \u4e26\u975e FiveM \u904a\u6232\u3002", + "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index fa85a0283d86a..87f2370aace16 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": ["fixerio==1.0.0a0"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["fixerio"] } diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 4d4d1882dcd23..da0a7f1dd2bbf 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -9,6 +9,7 @@ from bleak.backends.scanner import AdvertisementData from fjaraskupan import UUID_SERVICE, device_filter +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN @@ -16,7 +17,7 @@ CONST_WAIT_TIME = 5.0 -async def _async_has_devices(hass) -> bool: +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" event = asyncio.Event() diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index fb27d8b803f58..d01995bd28b86 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@elupus" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bleak", "fjaraskupan"] } \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/el.json b/homeassistant/components/fjaraskupan/translations/el.json index cb6a9ccddb233..fccba7806716e 100644 --- a/homeassistant/components/fjaraskupan/translations/el.json +++ b/homeassistant/components/fjaraskupan/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \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." }, "step": { "confirm": { diff --git a/homeassistant/components/fjaraskupan/translations/pt-BR.json b/homeassistant/components/fjaraskupan/translations/pt-BR.json new file mode 100644 index 0000000000000..164c5cde79332 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja configurar o Fj\u00e4r\u00e5skupan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/zh-Hant.json b/homeassistant/components/fjaraskupan/translations/zh-Hant.json index 3312cea3576a1..6a7db18da6170 100644 --- a/homeassistant/components/fjaraskupan/translations/zh-Hant.json +++ b/homeassistant/components/fjaraskupan/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index 4e4d1200e56fb..9f66c7e1cd71d 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": ["ritassist==0.9.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geopy", "ritassist"] } diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index 7480257fcaa06..bfbd919c05135 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": ["pyflic==2.0.3"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyflic"] } diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json index 75511aba4a1d9..0a79bff792ae9 100644 --- a/homeassistant/components/flick_electric/manifest.json +++ b/homeassistant/components/flick_electric/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/flick_electric/", "requirements": ["PyFlick==0.0.2"], "codeowners": ["@ZephireNZ"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyflick"] } diff --git a/homeassistant/components/flick_electric/translations/el.json b/homeassistant/components/flick_electric/translations/el.json new file mode 100644 index 0000000000000..7a237b4bf1c80 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/el.json @@ -0,0 +1,23 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "client_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "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" + }, + "title": "\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Flick" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/pt-BR.json b/homeassistant/components/flick_electric/translations/pt-BR.json index f23a27c2b73b1..444c72d3d5d3e 100644 --- a/homeassistant/components/flick_electric/translations/pt-BR.json +++ b/homeassistant/components/flick_electric/translations/pt-BR.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "Essa conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "client_id": "ID do cliente (Opcional)", - "client_secret": "Segredo do cliente (Opcional)", + "client_id": "Client ID (Opcional)", + "client_secret": "Client Secret (Opcional)", "password": "Senha", "username": "Usu\u00e1rio" }, diff --git a/homeassistant/components/flick_electric/translations/sk.json b/homeassistant/components/flick_electric/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 8379845982a8b..3281410ec2da3 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -3,6 +3,7 @@ import logging from flipr_api import FliprAPIRestClient +from flipr_api.exceptions import FliprError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform @@ -11,6 +12,7 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN, MANUFACTURER, NAME @@ -68,9 +70,14 @@ def __init__(self, hass, entry): async def _async_update_data(self): """Fetch data from API endpoint.""" - return await self.hass.async_add_executor_job( - self.client.get_pool_measure_latest, self.flipr_id - ) + try: + data = await self.hass.async_add_executor_job( + self.client.get_pool_measure_latest, self.flipr_id + ) + except (FliprError) as error: + raise UpdateFailed(error) from error + + return data class FliprEntity(CoordinatorEntity): diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 330fea7de8b96..77388393d3f40 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -4,9 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flipr", "requirements": [ - "flipr-api==1.4.1"], + "flipr-api==1.4.2"], "codeowners": [ "@cnico" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["flipr_api"] } diff --git a/homeassistant/components/flipr/translations/el.json b/homeassistant/components/flipr/translations/el.json new file mode 100644 index 0000000000000..122721cfc8ce8 --- /dev/null +++ b/homeassistant/components/flipr/translations/el.json @@ -0,0 +1,30 @@ +{ + "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", + "no_flipr_id_found": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc flipr \u03c0\u03bf\u03c5 \u03c3\u03c5\u03c3\u03c7\u03b5\u03c4\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd. \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c0\u03c1\u03ce\u03c4\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac \u03c4\u03bf\u03c5 Flipr.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc Flipr" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc Flipr \u03c3\u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf Flipr \u03c3\u03b1\u03c2" + }, + "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Flipr.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pt-BR.json b/homeassistant/components/flipr/translations/pt-BR.json new file mode 100644 index 0000000000000..e3535fb54ff8e --- /dev/null +++ b/homeassistant/components/flipr/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_flipr_id_found": "Nenhum ID flipr associado \u00e0 sua conta por enquanto. Voc\u00ea deve verificar se est\u00e1 funcionando com o aplicativo m\u00f3vel do Flipr primeiro.", + "unknown": "Erro inesperado" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "Escolha seu ID Flipr na lista", + "title": "Escolha seu Flipr" + }, + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "description": "Conecte-se usando sua conta Flipr.", + "title": "Conecte-se ao Flipr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/sk.json b/homeassistant/components/flipr/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/flipr/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json index 6d1e002012c4e..c93cd2bc6dd46 100644 --- a/homeassistant/components/flo/manifest.json +++ b/homeassistant/components/flo/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/flo", "requirements": ["aioflo==2021.11.0"], "codeowners": ["@dmulcahey"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aioflo"] } diff --git a/homeassistant/components/flo/translations/el.json b/homeassistant/components/flo/translations/el.json new file mode 100644 index 0000000000000..877622243c8a8 --- /dev/null +++ b/homeassistant/components/flo/translations/el.json @@ -0,0 +1,21 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/pt-BR.json b/homeassistant/components/flo/translations/pt-BR.json index 026edf7221c66..93beddb92a851 100644 --- a/homeassistant/components/flo/translations/pt-BR.json +++ b/homeassistant/components/flo/translations/pt-BR.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flo/translations/sk.json b/homeassistant/components/flo/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/flo/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index cdad0dd3f0c39..05b0a4bf19aef 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -10,5 +10,6 @@ "hostname": "flume-gw-*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyflume"] } diff --git a/homeassistant/components/flume/translations/cs.json b/homeassistant/components/flume/translations/cs.json index 23aa89e12f265..e3f6cf1d39e68 100644 --- a/homeassistant/components/flume/translations/cs.json +++ b/homeassistant/components/flume/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "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/flume/translations/el.json b/homeassistant/components/flume/translations/el.json index fca38446c23a2..a15da4ed9612a 100644 --- a/homeassistant/components/flume/translations/el.json +++ b/homeassistant/components/flume/translations/el.json @@ -1,11 +1,29 @@ { "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", + "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", + "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": { "reauth_confirm": { + "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 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Flume" }, "user": { + "data": { + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "client_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "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 \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc API \u03c4\u03bf\u03c5 Flume, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 'Client ID' \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 'Client Secret' \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://portal.flumetech.com/settings#token.", "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Flume" } diff --git a/homeassistant/components/flume/translations/pt-BR.json b/homeassistant/components/flume/translations/pt-BR.json index 033dc26c85607..a9027c5bfd64d 100644 --- a/homeassistant/components/flume/translations/pt-BR.json +++ b/homeassistant/components/flume/translations/pt-BR.json @@ -1,18 +1,28 @@ { "config": { "abort": { - "already_configured": "Essa conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} n\u00e3o \u00e9 mais v\u00e1lida.", + "title": "Reautentique sua conta Flume" + }, "user": { "data": { "client_id": "ID do Cliente", - "client_secret": "Segredo do cliente" + "client_secret": "Segredo do cliente", + "password": "Senha", + "username": "Usu\u00e1rio" }, "description": "Para acessar a API pessoal do Flume, voc\u00ea precisar\u00e1 solicitar um 'ID do Cliente' e 'Segredo do Cliente' em https://portal.flumetech.com/settings#token", "title": "Conecte-se \u00e0 sua conta Flume" diff --git a/homeassistant/components/flume/translations/sk.json b/homeassistant/components/flume/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/flume/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index 5fd3eb6638fe4..ee69961d1b0ca 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyflunearyou"] } diff --git a/homeassistant/components/flunearyou/translations/el.json b/homeassistant/components/flunearyou/translations/el.json new file mode 100644 index 0000000000000..972611ff6b1c1 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/el.json @@ -0,0 +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" + }, + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ce\u03bd \u03b2\u03ac\u03c3\u03b5\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 CDC \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Flu Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt-BR.json b/homeassistant/components/flunearyou/translations/pt-BR.json new file mode 100644 index 0000000000000..dc63fa1baf870 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Monitore relat\u00f3rios baseados em usu\u00e1rio e CDC para um par de coordenadas.", + "title": "Configurar Flue Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sk.json b/homeassistant/components/flunearyou/translations/sk.json new file mode 100644 index 0000000000000..e6945904d9030 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index ff1962aed1bd4..997f053aa3c10 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -90,8 +90,7 @@ async def _async_discovery(*_: Any) -> None: async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: """Migrate entities when the mac address gets discovered.""" - unique_id = entry.unique_id - if not unique_id: + if not (unique_id := entry.unique_id): return entry_id = entry.entry_id diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py index fcd4ecc3adcaf..bfbe63cf02e0d 100644 --- a/homeassistant/components/flux_led/button.py +++ b/homeassistant/components/flux_led/button.py @@ -63,7 +63,7 @@ def __init__( """Initialize the button.""" self.entity_description = description super().__init__(device, entry) - self._attr_name = f"{entry.data[CONF_NAME]} {description.name}" + self._attr_name = f"{entry.data.get(CONF_NAME, entry.title)} {description.name}" base_unique_id = entry.unique_id or entry.entry_id self._attr_unique_id = f"{base_unique_id}_{description.key}" diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index c6f14e929d6ce..5bdd18d1dbdf0 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -17,7 +17,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr @@ -81,10 +81,10 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes ) return await self._async_handle_discovery() - async def async_step_discovery( + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" + """Handle integration discovery.""" self._discovered_device = cast(FluxLEDDiscovery, discovery_info) return await self._async_handle_discovery() @@ -145,10 +145,7 @@ def _async_create_entry_from_device(self, device: FluxLEDDiscovery) -> FlowResul """Create a config entry from a device.""" self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]}) name = async_name_from_discovery(device) - data: dict[str, Any] = { - CONF_HOST: device[ATTR_IPADDR], - CONF_NAME: name, - } + data: dict[str, Any] = {CONF_HOST: device[ATTR_IPADDR]} async_populate_data_from_discovery(data, data, device) return self.async_create_entry( title=name, @@ -168,8 +165,7 @@ async def async_step_user( except FLUX_LED_EXCEPTIONS: errors["base"] = "cannot_connect" else: - mac_address = device[ATTR_ID] - if mac_address is not None: + if (mac_address := device[ATTR_ID]) is not None: await self.async_set_unique_id( dr.format_mac(mac_address), raise_on_progress=False ) diff --git a/homeassistant/components/flux_led/diagnostics.py b/homeassistant/components/flux_led/diagnostics.py new file mode 100644 index 0000000000000..f0c95ffbe5623 --- /dev/null +++ b/homeassistant/components/flux_led/diagnostics.py @@ -0,0 +1,24 @@ +"""Diagnostics support for flux_led.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import FluxLedUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + return { + "entry": { + "title": entry.title, + "data": dict(entry.data), + }, + "data": coordinator.device.diagnostics, + } diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 0f65c7c17974a..62b80243c8b85 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -82,8 +82,7 @@ def async_build_cached_discovery(entry: ConfigEntry) -> FluxLEDDiscovery: @callback def async_name_from_discovery(device: FluxLEDDiscovery) -> str: """Convert a flux_led discovery to a human readable name.""" - mac_address = device[ATTR_ID] - if mac_address is None: + if (mac_address := device[ATTR_ID]) is None: return device[ATTR_IPADDR] short_mac = mac_address[-6:] if device[ATTR_MODEL_DESCRIPTION]: @@ -125,10 +124,13 @@ def async_update_entry_from_discovery( if model_num and entry.data.get(CONF_MODEL_NUM) != model_num: data_updates[CONF_MODEL_NUM] = model_num async_populate_data_from_discovery(entry.data, data_updates, device) - if not entry.data.get(CONF_NAME) or is_ip_address(entry.data[CONF_NAME]): - updates["title"] = data_updates[CONF_NAME] = async_name_from_discovery(device) - if data_updates: + if is_ip_address(entry.title): + updates["title"] = async_name_from_discovery(device) + title_matches_name = entry.title == entry.data.get(CONF_NAME) + if data_updates or title_matches_name: updates["data"] = {**entry.data, **data_updates} + if title_matches_name: + del updates["data"][CONF_NAME] if updates: return hass.config_entries.async_update_entry(entry, **updates) return False @@ -210,7 +212,7 @@ def async_trigger_discovery( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={**device}, ) ) diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 5946ab817de20..da92931d1e685 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -40,7 +40,7 @@ def _async_device_info( ATTR_IDENTIFIERS: {(DOMAIN, entry.entry_id)}, ATTR_MANUFACTURER: "Zengge", ATTR_MODEL: device.model, - ATTR_NAME: entry.data[CONF_NAME], + ATTR_NAME: entry.data.get(CONF_NAME, entry.title), ATTR_SW_VERSION: sw_version_str, } if hw_model := entry.data.get(CONF_MODEL): diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 4534c45e22824..0d179cd2b7720 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -178,7 +178,7 @@ async def async_setup_entry( FluxLight( coordinator, entry.unique_id or entry.entry_id, - entry.data[CONF_NAME], + entry.data.get(CONF_NAME, entry.title), list(custom_effect_colors), options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED), options.get(CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL), diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 17c73200619b1..a1b541c177d71 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,11 +4,12 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.26"], + "requirements": ["flux_led==0.28.27"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", "dhcp": [ + {"registered_devices": true}, { "macaddress": "18B905*", "hostname": "[ba][lk]*" @@ -44,6 +45,7 @@ "macaddress": "C82E47*", "hostname": "sta*" } - ] + ], + "loggers": ["flux_led"] } diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index d7fad9cf0e6f4..b4e6e87a829f9 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -50,7 +50,7 @@ async def async_setup_entry( | FluxMusicPixelsPerSegmentNumber | FluxMusicSegmentsNumber ] = [] - name = entry.data[CONF_NAME] + name = entry.data.get(CONF_NAME, entry.title) base_unique_id = entry.unique_id or entry.entry_id if device.pixels_per_segment is not None: diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 3b78baa782b77..63929740020f9 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -53,7 +53,7 @@ async def async_setup_entry( | FluxRemoteConfigSelect | FluxWhiteChannelSelect ] = [] - name = entry.data[CONF_NAME] + name = entry.data.get(CONF_NAME, entry.title) base_unique_id = entry.unique_id or entry.entry_id if device.device_type == DeviceType.Switch: @@ -64,7 +64,7 @@ async def async_setup_entry( coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode" ) ) - if device.wirings: + if device.wirings and device.wiring is not None: entities.append( FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring") ) @@ -110,7 +110,7 @@ def __init__( ) -> None: """Initialize the power state select.""" super().__init__(device, entry) - self._attr_name = f"{entry.data[CONF_NAME]} Power Restored" + self._attr_name = f"{entry.data.get(CONF_NAME, entry.title)} Power Restored" base_unique_id = entry.unique_id or entry.entry_id self._attr_unique_id = f"{base_unique_id}_power_restored" self._async_set_current_option_from_device() @@ -237,7 +237,7 @@ def __init__( ) -> None: """Initialize the white channel select.""" super().__init__(device, entry) - self._attr_name = f"{entry.data[CONF_NAME]} White Channel" + self._attr_name = f"{entry.data.get(CONF_NAME, entry.title)} White Channel" base_unique_id = entry.unique_id or entry.entry_id self._attr_unique_id = f"{base_unique_id}_white_channel" diff --git a/homeassistant/components/flux_led/sensor.py b/homeassistant/components/flux_led/sensor.py index 18d1aac55067a..a4266e55fa891 100644 --- a/homeassistant/components/flux_led/sensor.py +++ b/homeassistant/components/flux_led/sensor.py @@ -26,7 +26,7 @@ async def async_setup_entry( FluxPairedRemotes( coordinator, entry.unique_id or entry.entry_id, - f"{entry.data[CONF_NAME]} Paired Remotes", + f"{entry.data.get(CONF_NAME, entry.title)} Paired Remotes", "paired_remotes", ) ] diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index ee004fc2250ff..e8c34f12b118c 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -35,7 +35,7 @@ async def async_setup_entry( coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = [] base_unique_id = entry.unique_id or entry.entry_id - name = entry.data[CONF_NAME] + name = entry.data.get(CONF_NAME, entry.title) if coordinator.device.device_type == DeviceType.Switch: entities.append(FluxSwitch(coordinator, base_unique_id, name, None)) @@ -73,7 +73,7 @@ def __init__( ) -> None: """Initialize the light.""" super().__init__(device, entry) - self._attr_name = f"{entry.data[CONF_NAME]} Remote Access" + self._attr_name = f"{entry.data.get(CONF_NAME, entry.title)} Remote Access" base_unique_id = entry.unique_id or entry.entry_id self._attr_unique_id = f"{base_unique_id}_remote_access" diff --git a/homeassistant/components/flux_led/translations/el.json b/homeassistant/components/flux_led/translations/el.json new file mode 100644 index 0000000000000..903d73287b46a --- /dev/null +++ b/homeassistant/components/flux_led/translations/el.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} {id} ({ipaddr});" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03c6\u03ad: \u039b\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 1 \u03ad\u03c9\u03c2 16 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03b1 [R,G,B]. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03c6\u03ad: \u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03c3\u03b5 \u03c0\u03bf\u03c3\u03bf\u03c3\u03c4\u03ac \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03c6\u03ad \u03c0\u03bf\u03c5 \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03b1.", + "custom_effect_transition": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03c6\u03ad: \u03a4\u03cd\u03c0\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03c7\u03c1\u03c9\u03bc\u03ac\u03c4\u03c9\u03bd.", + "mode": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pt-BR.json b/homeassistant/components/flux_led/translations/pt-BR.json new file mode 100644 index 0000000000000..36560315ee740 --- /dev/null +++ b/homeassistant/components/flux_led/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "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" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Efeito personalizado: Lista de 1 a 16 cores [R,G,B]. Exemplo: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Efeito personalizado: Velocidade em porcentagens para o efeito que muda de cor.", + "custom_effect_transition": "Efeito Personalizado: Tipo de transi\u00e7\u00e3o entre as cores.", + "mode": "O modo de brilho escolhido." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/sk.json b/homeassistant/components/flux_led/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/flux_led/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index c243c0d45c82a..fb9a9ea5d63e4 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -5,5 +5,6 @@ "requirements": ["watchdog==2.1.6"], "codeowners": [], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["watchdog"] } diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index b32ff6b4c8aab..4bef77aee8a26 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": ["foobot_async==1.0.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["foobot_async"] } diff --git a/homeassistant/components/forecast_solar/translations/cs.json b/homeassistant/components/forecast_solar/translations/cs.json index 0b970643bbef2..d1c9b95470f81 100644 --- a/homeassistant/components/forecast_solar/translations/cs.json +++ b/homeassistant/components/forecast_solar/translations/cs.json @@ -3,10 +3,28 @@ "step": { "user": { "data": { + "azimuth": "Azimut (360\u02da, 0 = sever, 90 = v\u00fdchod, 180 = jih, 270 = z\u00e1pad)", + "declination": "Deklinace (0 = horizont\u00e1ln\u00ed, 90 = vertik\u00e1ln\u00ed)", "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "modules power": "Celkov\u00fd \u0161pi\u010dkov\u00fd v\u00fdkon sol\u00e1rn\u00edch modul\u016f", "name": "Jm\u00e9no" - } + }, + "description": "Vypl\u0148te \u00fadaje o sv\u00fdch sol\u00e1rn\u00edch panelech. Pokud je n\u011bkter\u00e9 pole nejasn\u00e9, nahl\u00e9dn\u011bte do dokumentace." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API kl\u00ed\u010d (voliteln\u00fd)", + "azimuth": "Azimut (360\u02da, 0 = sever, 90 = v\u00fdchod, 180 = jih, 270 = z\u00e1pad)", + "damping": "\u010cinitel tlumen\u00ed: upravuje v\u00fdsledky r\u00e1no a ve\u010der", + "declination": "Deklinace (0 = horizont\u00e1ln\u00ed, 90 = vertik\u00e1ln\u00ed)", + "modules power": "Celkov\u00fd \u0161pi\u010dkov\u00fd v\u00fdkon sol\u00e1rn\u00edch modul\u016f" + }, + "description": "Tyto hodnoty umo\u017e\u0148uj\u00ed vyladit v\u00fdsledn\u00e9 hodnoty Solar.Forecast. Pokud n\u011bkter\u00e9 pole nen\u00ed jasn\u00e9, nahl\u00e9dn\u011bte do dokumentace." } } } diff --git a/homeassistant/components/forecast_solar/translations/el.json b/homeassistant/components/forecast_solar/translations/el.json new file mode 100644 index 0000000000000..0f6603a622ba9 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/el.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "\u0391\u03b6\u03b9\u03bc\u03bf\u03cd\u03b8\u03b9\u03bf (360 \u03bc\u03bf\u03af\u03c1\u03b5\u03c2, 0 = \u0392\u03bf\u03c1\u03c1\u03ac\u03c2, 90 = \u0391\u03bd\u03b1\u03c4\u03bf\u03bb\u03ae, 180 = \u039d\u03cc\u03c4\u03bf\u03c2, 270 = \u0394\u03cd\u03c3\u03b7)", + "declination": "\u0391\u03c0\u03cc\u03ba\u03bb\u03b9\u03c3\u03b7 (0 = \u03bf\u03c1\u03b9\u03b6\u03cc\u03bd\u03c4\u03b9\u03b1, 90 = \u03ba\u03b1\u03c4\u03b1\u03ba\u03cc\u03c1\u03c5\u03c6\u03b7)", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "modules power": "\u03a3\u03c5\u03bd\u03bf\u03bb\u03b9\u03ba\u03ae \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b9\u03c3\u03c7\u03cd\u03c2 Watt \u03c4\u03c9\u03bd \u03b7\u03bb\u03b9\u03b1\u03ba\u03ce\u03bd \u03c3\u03b1\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03c9\u03bd", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03c9\u03bd \u03b7\u03bb\u03b9\u03b1\u03ba\u03ce\u03bd \u03c3\u03b1\u03c2 \u03c3\u03c5\u03bb\u03bb\u03b5\u03ba\u03c4\u03ce\u03bd. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03ad\u03bd\u03b1 \u03c0\u03b5\u03b4\u03af\u03bf \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c3\u03b1\u03c6\u03ad\u03c2." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API Key (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "azimuth": "\u0391\u03b6\u03b9\u03bc\u03bf\u03cd\u03b8\u03b9\u03bf (360 \u03bc\u03bf\u03af\u03c1\u03b5\u03c2, 0 = \u0392\u03bf\u03c1\u03c1\u03ac\u03c2, 90 = \u0391\u03bd\u03b1\u03c4\u03bf\u03bb\u03ae, 180 = \u039d\u03cc\u03c4\u03bf\u03c2, 270 = \u0394\u03cd\u03c3\u03b7)", + "damping": "\u03a3\u03c5\u03bd\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03ae\u03c2 \u03b1\u03c0\u03cc\u03c3\u03b2\u03b5\u03c3\u03b7\u03c2: \u03c1\u03c5\u03b8\u03bc\u03af\u03b6\u03b5\u03b9 \u03c4\u03b1 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1 \u03c0\u03c1\u03c9\u03af \u03ba\u03b1\u03b9 \u03b2\u03c1\u03ac\u03b4\u03c5", + "declination": "\u0391\u03c0\u03cc\u03ba\u03bb\u03b9\u03c3\u03b7 (0 = \u03bf\u03c1\u03b9\u03b6\u03cc\u03bd\u03c4\u03b9\u03b1, 90 = \u03ba\u03b1\u03c4\u03b1\u03ba\u03cc\u03c1\u03c5\u03c6\u03b7)", + "modules power": "\u03a3\u03c5\u03bd\u03bf\u03bb\u03b9\u03ba\u03ae \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b9\u03c3\u03c7\u03cd\u03c2 Watt \u03c4\u03c9\u03bd \u03b7\u03bb\u03b9\u03b1\u03ba\u03ce\u03bd \u03c3\u03b1\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03c9\u03bd" + }, + "description": "\u0391\u03c5\u03c4\u03ad\u03c2 \u03bf\u03b9 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03c5\u03bd \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Solar.Forecast. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b1\u03c6\u03ad\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/pt-BR.json b/homeassistant/components/forecast_solar/translations/pt-BR.json new file mode 100644 index 0000000000000..ad6cca066c4f4 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimute (360\u00b0, 0\u00b0 = Norte, 90\u00b0 = Leste, 180\u00b0 = Sul, 270\u00b0 = Oeste)", + "declination": "Declina\u00e7\u00e3o (0\u00b0 = Horizontal, 90\u00b0 = Vertical)", + "latitude": "Latitude", + "longitude": "Longitude", + "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." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Chave de API Forecast.Solar (opcional)", + "azimuth": "Azimute (360\u00b0, 0\u00b0 = Norte, 90\u00b0 = Leste, 180\u00b0 = Sul, 270\u00b0 = Oeste)", + "damping": "Fator de amortecimento: ajusta os resultados de manh\u00e3 e \u00e0 noite", + "declination": "Declina\u00e7\u00e3o (0\u00b0 = Horizontal, 90\u00b0 = Vertical)", + "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." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/sk.json b/homeassistant/components/forecast_solar/translations/sk.json new file mode 100644 index 0000000000000..939157fb83769 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/sk.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "azimuth": "Azimut (360\u02da, 0 = sever, 90 = v\u00fdchod, 180 = juh, 270 = z\u00e1pad)", + "declination": "Deklin\u00e1cia (0 = horizont\u00e1lna, 90 = vertik\u00e1lna)", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "modules power": "Celkov\u00fd \u0161pi\u010dkov\u00fd v\u00fdkon sol\u00e1rnych modulov", + "name": "N\u00e1zov" + }, + "description": "Vypl\u0148te \u00fadaje o svojich sol\u00e1rnych paneloch. Ak je niektor\u00e9 pole nejasn\u00e9, pozrite si dokument\u00e1ciu." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "Forecast.Solar API k\u013e\u00fa\u010d (volite\u013en\u00fd)", + "azimuth": "Azimut (360\u02da, 0 = sever, 90 = v\u00fdchod, 180 = juh, 270 = z\u00e1pad)", + "damping": "\u010cinite\u013e tlmenia: upravuje v\u00fdsledky r\u00e1no a ve\u010der", + "declination": "Deklin\u00e1cia (0 = horizont\u00e1lna, 90 = vertik\u00e1lna)", + "modules power": "Celkov\u00fd \u0161pi\u010dkov\u00fd v\u00fdkon sol\u00e1rnych modulov" + }, + "description": "Tieto hodnoty umo\u017e\u0148uj\u00fa upravi\u0165 v\u00fdsledn\u00e9 hodnoty Solar.Forecast. Ak je niektor\u00e9 pole nejasn\u00e9, pozrite si dokument\u00e1ciu." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index b802eac13c8d2..9a0372a193e60 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], "config_flow": true, "zeroconf": ["_daap._tcp.local."], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyforked_daapd", "pylibrespot_java"] } diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index e1b21efe541a5..a68f56e296527 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -115,7 +115,7 @@ def async_add_zones(api, outputs): ] = forked_daapd_updater -async def update_listener(hass, entry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" async_dispatcher_send( hass, SIGNAL_CONFIG_OPTIONS_UPDATE.format(entry.entry_id), entry.options diff --git a/homeassistant/components/forked_daapd/translations/el.json b/homeassistant/components/forked_daapd/translations/el.json index a1e23fa41459e..ec29c3de039e1 100644 --- a/homeassistant/components/forked_daapd/translations/el.json +++ b/homeassistant/components/forked_daapd/translations/el.json @@ -1,11 +1,41 @@ { + "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", + "not_forked_daapd": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 forked-daapd." + }, + "error": { + "forbidden": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c4\u03bf\u03c5 forked-daapd.", + "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "websocket_not_enabled": "\u039f forked-daapd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 websocket \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2.", + "wrong_host_or_port": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1.", + "wrong_password": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", + "wrong_server_type": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 forked-daapd \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae forked-daapd \u03bc\u03b5 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 >= 27.0." + }, + "flow_title": "{name} ({host})", + "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": "\u03a6\u03b9\u03bb\u03b9\u03ba\u03cc \u03cc\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 API (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2)", + "port": "\u0398\u03cd\u03c1\u03b1 API" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 forked-daapd" + } + } + }, "options": { "step": { "init": { "data": { - "librespot_java_port": "\u0398\u03cd\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c3\u03c9\u03bb\u03ae\u03bd\u03b1 librespot-java (\u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9)" + "librespot_java_port": "\u0398\u03cd\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c3\u03c9\u03bb\u03ae\u03bd\u03b1 librespot-java (\u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9)", + "max_playlists": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bb\u03b9\u03c3\u03c4\u03ce\u03bd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b7\u03b3\u03ad\u03c2", + "tts_pause_time": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03cd\u03c3\u03b7 \u03c0\u03c1\u03b9\u03bd \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c4\u03bf TTS", + "tts_volume": "\u0388\u03bd\u03c4\u03b1\u03c3\u03b7 TTS (\u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [0,1])" }, - "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 forked-daapd." + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 forked-daapd.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd forked-daapd" } } } diff --git a/homeassistant/components/forked_daapd/translations/pt-BR.json b/homeassistant/components/forked_daapd/translations/pt-BR.json index c45178d6a7219..1b768604befe4 100644 --- a/homeassistant/components/forked_daapd/translations/pt-BR.json +++ b/homeassistant/components/forked_daapd/translations/pt-BR.json @@ -1,21 +1,22 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado.", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "not_forked_daapd": "O dispositivo n\u00e3o \u00e9 um servidor forked-daapd." }, "error": { - "unknown_error": "Erro desconhecido.", + "forbidden": "Incapaz de conectar. Verifique suas permiss\u00f5es de rede forked-daapd.", + "unknown_error": "Erro inesperado", "websocket_not_enabled": "websocket do servidor forked-daapd n\u00e3o ativado.", "wrong_host_or_port": "N\u00e3o foi poss\u00edvel conectar. Por favor, verifique o endere\u00e7o e a porta.", "wrong_password": "Senha incorreta.", "wrong_server_type": "A integra\u00e7\u00e3o forked-daapd requer um servidor forked-daapd com vers\u00e3o >= 27.0." }, - "flow_title": "servidor forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Endere\u00e7o (IP)", + "host": "Nome do host", "name": "Nome amig\u00e1vel", "password": "Senha da API (deixe em branco se n\u00e3o houver senha)", "port": "Porta API" diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index cc351441cdd56..c7084d4cab470 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": ["fortiosapi==1.0.5"], "codeowners": ["@kimfrellsen"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["fortiosapi", "paramiko"] } diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index e2d9e5e501daa..39103e3ea3ee5 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": ["libpyfoscam==1.0"], "codeowners": ["@skgsergio"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["libpyfoscam"] } diff --git a/homeassistant/components/foscam/translations/el.json b/homeassistant/components/foscam/translations/el.json index 7022315d9c490..0e6c9b7c65dbd 100644 --- a/homeassistant/components/foscam/translations/el.json +++ b/homeassistant/components/foscam/translations/el.json @@ -1,13 +1,23 @@ { "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": { - "invalid_response": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + "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_response": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { "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", + "port": "\u0398\u03cd\u03c1\u03b1", "rtsp_port": "\u0398\u03cd\u03c1\u03b1 RTSP", - "stream": "\u03a1\u03bf\u03ae" + "stream": "\u03a1\u03bf\u03ae", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } } diff --git a/homeassistant/components/foscam/translations/pt-BR.json b/homeassistant/components/foscam/translations/pt-BR.json new file mode 100644 index 0000000000000..b33dce1e6feac --- /dev/null +++ b/homeassistant/components/foscam/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_response": "Resposta inv\u00e1lida do dispositivo", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "rtsp_port": "porta RTSP", + "stream": "Stream", + "username": "Usu\u00e1rio" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/sk.json b/homeassistant/components/foscam/translations/sk.json new file mode 100644 index 0000000000000..8bbcb516b5626 --- /dev/null +++ b/homeassistant/components/foscam/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index 7fb7f9986432d..db3144e83e86c 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": ["freesms==0.2.0"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["freesms"] } diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index ee8c2f3097596..7d7bc7695cd06 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,18 +1,20 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from datetime import timedelta import logging +from freebox_api.exceptions import HttpRequestError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT -from .router import FreeboxRouter - -_LOGGER = logging.getLogger(__name__) +from .router import FreeboxRouter, get_api FREEBOX_SCHEMA = vol.Schema( {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} @@ -26,6 +28,10 @@ extra=vol.ALLOW_EXTRA, ) +SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Freebox integration.""" @@ -42,8 +48,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Freebox entry.""" - router = FreeboxRouter(hass, entry) - await router.setup() + api = await get_api(hass, entry.data[CONF_HOST]) + try: + await api.open(entry.data[CONF_HOST], entry.data[CONF_PORT]) + except HttpRequestError as err: + raise ConfigEntryNotReady from err + + freebox_config = await api.system.get_config() + + router = FreeboxRouter(hass, entry, api, freebox_config) + await router.update_all() + entry.async_on_unload( + async_track_time_interval(hass, router.update_all, SCAN_INTERVAL) + ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router @@ -53,11 +70,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Services async def async_reboot(call: ServiceCall) -> None: """Handle reboot service call.""" + # The Freebox reboot service has been replaced by a + # dedicated button entity and marked as deprecated + _LOGGER.warning( + "The 'freebox.reboot' service is deprecated and " + "replaced by a dedicated reboot button entity; please " + "use that entity to reboot the freebox instead" + ) await router.reboot() hass.services.async_register(DOMAIN, SERVICE_REBOOT, async_reboot) - async def async_close_connection(event): + async def async_close_connection(event: Event) -> None: """Close Freebox connection on HA Stop.""" await router.close() @@ -72,7 +96,7 @@ 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: - router = hass.data[DOMAIN].pop(entry.unique_id) + router: FreeboxRouter = hass.data[DOMAIN].pop(entry.unique_id) await router.close() hass.services.async_remove(DOMAIN, SERVICE_REBOOT) diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py new file mode 100644 index 0000000000000..b3313bba9ddce --- /dev/null +++ b/homeassistant/components/freebox/button.py @@ -0,0 +1,76 @@ +"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +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 .const import DOMAIN +from .router import FreeboxRouter + + +@dataclass +class FreeboxButtonRequiredKeysMixin: + """Mixin for required keys.""" + + async_press: Callable[[FreeboxRouter], Awaitable] + + +@dataclass +class FreeboxButtonEntityDescription( + ButtonEntityDescription, FreeboxButtonRequiredKeysMixin +): + """Class describing Freebox button entities.""" + + +BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = ( + FreeboxButtonEntityDescription( + key="reboot", + name="Reboot Freebox", + device_class=ButtonDeviceClass.RESTART, + async_press=lambda router: router.reboot(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the buttons.""" + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + entities = [ + FreeboxButton(router, description) for description in BUTTON_DESCRIPTIONS + ] + async_add_entities(entities, True) + + +class FreeboxButton(ButtonEntity): + """Representation of a Freebox button.""" + + entity_description: FreeboxButtonEntityDescription + + def __init__( + self, router: FreeboxRouter, description: FreeboxButtonEntityDescription + ) -> None: + """Initialize a Freebox button.""" + self.entity_description = description + self._router = router + self._attr_unique_id = f"{router.mac} {description.name}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return self._router.device_info + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.async_press(self._router) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 77f36cf44deb2..65e5576f9d7ad 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -17,7 +17,7 @@ } API_VERSION = "v6" -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] DEFAULT_DEVICE_NAME = "Unknown device" diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index d57af2d53c25c..0fe04fe7eb979 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -19,15 +19,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for Freebox component.""" - router = hass.data[DOMAIN][entry.unique_id] - tracked = set() + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + tracked: set[str] = set() @callback - def update_router(): + def update_router() -> None: """Update the values of the router.""" add_entities(router, async_add_entities, tracked) - router.listeners.append( + entry.async_on_unload( async_dispatcher_connect(hass, router.signal_device_new, update_router) ) @@ -35,7 +35,9 @@ def update_router(): @callback -def add_entities(router, async_add_entities, tracked): +def add_entities( + router: FreeboxRouter, async_add_entities: AddEntitiesCallback, tracked: set[str] +) -> None: """Add new tracker entities from the router.""" new_tracked = [] @@ -61,7 +63,7 @@ def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None: self._manufacturer = device["vendor_name"] self._icon = icon_for_freebox_device(device) self._active = False - self._attrs = {} + self._attrs: dict[str, Any] = {} @callback def async_update_state(self) -> None: diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 254be7b685766..846bff5f8ce43 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -6,5 +6,6 @@ "requirements": ["freebox-api==0.0.10"], "zeroconf": ["_fbx-api._tcp.local."], "codeowners": ["@hacf-fr", "@Quentame"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["freebox_api"] } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index e352146915e94..0d20545fdcd90 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -1,24 +1,23 @@ """Represent the Freebox router and its devices and sensors.""" from __future__ import annotations -from datetime import datetime, timedelta -import logging +from collections.abc import Mapping +from contextlib import suppress +from datetime import datetime import os from pathlib import Path from typing import Any from freebox_api import Freepybox from freebox_api.api.wifi import Wifi -from freebox_api.exceptions import HttpRequestError +from freebox_api.exceptions import NotOpenError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify from .const import ( @@ -30,10 +29,6 @@ STORAGE_VERSION, ) -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=30) - async def get_api(hass: HomeAssistant, host: str) -> Freepybox: """Get the Freebox API.""" @@ -50,18 +45,23 @@ async def get_api(hass: HomeAssistant, host: str) -> Freepybox: class FreeboxRouter: """Representation of a Freebox router.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: Freepybox, + freebox_config: Mapping[str, Any], + ) -> None: """Initialize a Freebox router.""" self.hass = hass - self._entry = entry self._host = entry.data[CONF_HOST] self._port = entry.data[CONF_PORT] - self._api: Freepybox = None - self.name = None - self.mac = None - self._sw_v = None - self._attrs = {} + self._api: Freepybox = api + self.name: str = freebox_config["model_info"]["pretty_name"] + self.mac: str = freebox_config["mac"] + self._sw_v: str = freebox_config["firmware_version"] + self._attrs: dict[str, Any] = {} self.devices: dict[str, dict[str, Any]] = {} self.disks: dict[int, dict[str, Any]] = {} @@ -69,31 +69,6 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self.sensors_connection: dict[str, float] = {} self.call_list: list[dict[str, Any]] = [] - self._unsub_dispatcher = None - self.listeners = [] - - async def setup(self) -> None: - """Set up a Freebox router.""" - self._api = await get_api(self.hass, self._host) - - try: - await self._api.open(self._host, self._port) - except HttpRequestError: - _LOGGER.exception("Failed to connect to Freebox") - return ConfigEntryNotReady - - # System - fbx_config = await self._api.system.get_config() - self.mac = fbx_config["mac"] - self.name = fbx_config["model_info"]["pretty_name"] - self._sw_v = fbx_config["firmware_version"] - - # Devices & sensors - await self.update_all() - self._unsub_dispatcher = async_track_time_interval( - self.hass, self.update_all, SCAN_INTERVAL - ) - async def update_all(self, now: datetime | None = None) -> None: """Update all Freebox platforms.""" await self.update_device_trackers() @@ -102,7 +77,7 @@ async def update_all(self, now: datetime | None = None) -> None: async def update_device_trackers(self) -> None: """Update Freebox devices.""" new_device = False - fbx_devices: [dict[str, Any]] = await self._api.lan.get_hosts_list() + fbx_devices: list[dict[str, Any]] = await self._api.lan.get_hosts_list() # Adds the Freebox itself fbx_devices.append( @@ -164,7 +139,7 @@ async def update_sensors(self) -> None: async def _update_disks_sensors(self) -> None: """Update Freebox disks.""" # None at first request - fbx_disks: [dict[str, Any]] = await self._api.storage.get_disks() or [] + fbx_disks: list[dict[str, Any]] = await self._api.storage.get_disks() or [] for fbx_disk in fbx_disks: self.disks[fbx_disk["id"]] = fbx_disk @@ -175,10 +150,8 @@ async def reboot(self) -> None: async def close(self) -> None: """Close the connection.""" - if self._api is not None: + with suppress(NotOpenError): await self._api.close() - self._unsub_dispatcher() - self._api = None @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index c5852aa22aafc..46aa9ee8aa079 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" - router = hass.data[DOMAIN][entry.unique_id] + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] entities = [] _LOGGER.debug( @@ -120,7 +120,7 @@ def __init__( ) -> None: """Initialize a Freebox call sensor.""" super().__init__(router, description) - self._call_list_for_type = [] + self._call_list_for_type: list[dict[str, Any]] = [] @callback def async_update_state(self) -> None: diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 76b89a1741471..ee59c097f9371 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -21,7 +21,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the switch.""" - router = hass.data[DOMAIN][entry.unique_id] + router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] async_add_entities([FreeboxWifiSwitch(router)], True) @@ -31,7 +31,7 @@ class FreeboxWifiSwitch(SwitchEntity): def __init__(self, router: FreeboxRouter) -> None: """Initialize the Wifi switch.""" self._name = "Freebox WiFi" - self._state = None + self._state: bool | None = None self._router = router self._unique_id = f"{self._router.mac} {self._name}" @@ -46,7 +46,7 @@ def name(self) -> str: return self._name @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state diff --git a/homeassistant/components/freebox/translations/el.json b/homeassistant/components/freebox/translations/el.json new file mode 100644 index 0000000000000..e881230014b1d --- /dev/null +++ b/homeassistant/components/freebox/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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "register_failed": "\u0397 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "link": { + "description": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae\" \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b1\u03b3\u03b3\u03af\u03be\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03be\u03af \u03b2\u03ad\u03bb\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Freebox \u03c3\u03c4\u03bf Home Assistant.\n\n![\u0398\u03ad\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae](/static/images/config_freebox.png)", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Freebox" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/pt-BR.json b/homeassistant/components/freebox/translations/pt-BR.json new file mode 100644 index 0000000000000..021ab5c902ad5 --- /dev/null +++ b/homeassistant/components/freebox/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "register_failed": "Falha ao registrar, tente novamente", + "unknown": "Erro inesperado" + }, + "step": { + "link": { + "description": "Clique em \"Enviar\" e toque na seta para a direita no roteador para registrar o Freebox com o Home Assistant.\n\n![Localiza\u00e7\u00e3o do bot\u00e3o no roteador](/static/images/config_freebox.png)", + "title": "Link roteador Freebox" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sk.json b/homeassistant/components/freebox/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/freebox/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 327b6314a3454..ec0085c9d329a 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -55,7 +55,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def update_listener(hass, config_entry): +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/freedompro/manifest.json b/homeassistant/components/freedompro/manifest.json index 94d57b37cae3f..174862712688f 100644 --- a/homeassistant/components/freedompro/manifest.json +++ b/homeassistant/components/freedompro/manifest.json @@ -7,5 +7,6 @@ "@stefano055415" ], "requirements": ["pyfreedompro==1.1.0"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyfreedompro"] } diff --git a/homeassistant/components/freedompro/translations/el.json b/homeassistant/components/freedompro/translations/el.json new file mode 100644 index 0000000000000..cb39329ebab09 --- /dev/null +++ b/homeassistant/components/freedompro/translations/el.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c0\u03bf\u03c5 \u03bb\u03ac\u03b2\u03b1\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://home.freedompro.eu", + "title": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/pt-BR.json b/homeassistant/components/freedompro/translations/pt-BR.json new file mode 100644 index 0000000000000..2e20c6b5da9f7 --- /dev/null +++ b/homeassistant/components/freedompro/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + }, + "description": "Insira a chave de API obtida em https://home.freedompro.eu", + "title": "Chave da API Freedompro" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/sk.json b/homeassistant/components/freedompro/translations/sk.json new file mode 100644 index 0000000000000..ff85312780312 --- /dev/null +++ b/homeassistant/components/freedompro/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b005e536ed63e..b2a429bfa3c11 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -18,6 +18,7 @@ ) from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus +from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.const import ( @@ -38,6 +39,8 @@ from homeassistant.util import dt as dt_util from .const import ( + CONF_OLD_DISCOVERY, + DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_DEVICE_NAME, DEFAULT_HOST, DEFAULT_PORT, @@ -47,6 +50,7 @@ SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, + SERVICE_SET_GUEST_WIFI_PW, MeshRoles, ) @@ -150,6 +154,7 @@ def __init__( self._options: MappingProxyType[str, Any] | None = None self._unique_id: str | None = None self.connection: FritzConnection = None + self.fritz_guest_wifi: FritzGuestWLAN = None self.fritz_hosts: FritzHosts = None self.fritz_status: FritzStatus = None self.hass = hass @@ -194,6 +199,7 @@ def setup(self) -> None: ) self.fritz_hosts = FritzHosts(fc=self.connection) + self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) info = self.connection.call_action("DeviceInfo:1", "GetInfo") @@ -321,26 +327,33 @@ async def async_scan_devices(self, now: datetime | None = None) -> None: """Wrap up FritzboxTools class scan.""" await self.hass.async_add_executor_job(self.scan_devices, now) + def manage_device_info( + self, dev_info: Device, dev_mac: str, consider_home: bool + ) -> bool: + """Update device lists.""" + _LOGGER.debug("Client dev_info: %s", dev_info) + + if dev_mac in self._devices: + self._devices[dev_mac].update(dev_info, consider_home) + return False + + device = FritzDevice(dev_mac, dev_info.name) + device.update(dev_info, consider_home) + self._devices[dev_mac] = device + return True + + def send_signal_device_update(self, new_device: bool) -> None: + """Signal device data updated.""" + dispatcher_send(self.hass, self.signal_device_update) + if new_device: + dispatcher_send(self.hass, self.signal_device_new) + def scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) self._update_available, self._latest_firmware = self._update_device_info() - if ( - "Hosts1" not in self.connection.services - or "X_AVM-DE_GetMeshListPath" - not in self.connection.services["Hosts1"].actions - ): - self.mesh_role = MeshRoles.NONE - else: - try: - topology = self.fritz_hosts.get_mesh_topology() - except FritzActionError: - self.mesh_role = MeshRoles.SLAVE - # Avoid duplicating device trackers - return - _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() if self._options: @@ -366,9 +379,35 @@ def scan_devices(self, now: datetime | None = None) -> None: wan_access=None, ) + if ( + "Hosts1" not in self.connection.services + or "X_AVM-DE_GetMeshListPath" + not in self.connection.services["Hosts1"].actions + ) or ( + self._options + and self._options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY) + ): + _LOGGER.debug( + "Using old hosts discovery method. (Mesh not supported or user option)" + ) + self.mesh_role = MeshRoles.NONE + for mac, info in hosts.items(): + if self.manage_device_info(info, mac, consider_home): + new_device = True + self.send_signal_device_update(new_device) + return + + try: + if not (topology := self.fritz_hosts.get_mesh_topology()): + raise Exception("Mesh supported but empty topology reported") + except FritzActionError: + self.mesh_role = MeshRoles.SLAVE + # Avoid duplicating device trackers + return + mesh_intf = {} # first get all meshed devices - for node in topology["nodes"]: + for node in topology.get("nodes", []): if not node["is_meshed"]: continue @@ -385,7 +424,7 @@ def scan_devices(self, now: datetime | None = None) -> None: self.mesh_role = MeshRoles(node["mesh_role"]) # second get all client devices - for node in topology["nodes"]: + for node in topology.get("nodes", []): if node["is_meshed"]: continue @@ -409,19 +448,11 @@ def scan_devices(self, now: datetime | None = None) -> None: dev_info.connected_to = intf["device"] dev_info.connection_type = intf["type"] dev_info.ssid = intf.get("ssid") - _LOGGER.debug("Client dev_info: %s", dev_info) - - if dev_mac in self._devices: - self._devices[dev_mac].update(dev_info, consider_home) - else: - device = FritzDevice(dev_mac, dev_info.name) - device.update(dev_info, consider_home) - self._devices[dev_mac] = device + + if self.manage_device_info(dev_info, dev_mac, consider_home): new_device = True - dispatcher_send(self.hass, self.signal_device_update) - if new_device: - dispatcher_send(self.hass, self.signal_device_new) + self.send_signal_device_update(new_device) async def async_trigger_firmware_update(self) -> bool: """Trigger firmware update.""" @@ -438,6 +469,14 @@ async def async_trigger_reconnect(self) -> None: """Trigger device reconnect.""" await self.hass.async_add_executor_job(self.connection.reconnect) + async def async_trigger_set_guest_password( + self, password: str | None, length: int + ) -> None: + """Trigger service to set a new guest wifi password.""" + await self.hass.async_add_executor_job( + self.fritz_guest_wifi.set_password, password, length + ) + async def async_trigger_cleanup( self, config_entry: ConfigEntry | None = None ) -> None: @@ -537,6 +576,13 @@ async def service_fritzbox( await self.async_trigger_cleanup(config_entry) return + if service_call.service == SERVICE_SET_GUEST_WIFI_PW: + await self.async_trigger_set_guest_password( + service_call.data.get("password"), + service_call.data.get("length", DEFAULT_PASSWORD_LENGTH), + ) + return + except (FritzServiceError, FritzActionError) as ex: raise HomeAssistantError("Service or parameter unknown") from ex except FritzConnectionException as ex: diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 180a6239d9fb8..0844d725522a0 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -21,6 +21,8 @@ from .common import AvmWrapper from .const import ( + CONF_OLD_DISCOVERY, + DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_HOST, DEFAULT_PORT, DOMAIN, @@ -107,6 +109,7 @@ def _async_create_entry(self) -> FlowResult: }, options={ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), + CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY, }, ) @@ -296,6 +299,12 @@ async def async_step_init( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() ), ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)), + vol.Optional( + CONF_OLD_DISCOVERY, + default=self.config_entry.options.get( + CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 59200e07c782b..f33cf4639964e 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -32,6 +32,9 @@ class MeshRoles(StrEnum): Platform.SWITCH, ] +CONF_OLD_DISCOVERY = "old_discovery" +DEFAULT_CONF_OLD_DISCOVERY = False + DATA_FRITZ = "fritz_data" DSL_CONNECTION: Literal["dsl"] = "dsl" @@ -49,6 +52,7 @@ class MeshRoles(StrEnum): SERVICE_REBOOT = "reboot" SERVICE_RECONNECT = "reconnect" SERVICE_CLEANUP = "cleanup" +SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password" SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" @@ -63,3 +67,5 @@ class MeshRoles(StrEnum): FritzServiceError, FritzLookUpError, ) + +WIFI_STANDARD = {1: "2.4Ghz", 2: "5Ghz", 3: "5Ghz", 4: "Guest"} diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 9e553abcd623f..b278bd8d1961f 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -19,5 +19,6 @@ "st": "urn:schemas-upnp-org:device:fritzbox:1" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["fritzconnection"] } diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 5e4b18eebcaf1..f01966d7114d8 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta import logging -from typing import Any, Literal +from typing import Any from fritzconnection.core.exceptions import FritzConnectionException from fritzconnection.lib.fritzstatus import FritzStatus @@ -134,6 +134,15 @@ def _retrieve_link_attenuation_received_state( return status.attenuation[1] / 10 # type: ignore[no-any-return] +@dataclass +class ConnectionInfo: + """Fritz sensor connection information class.""" + + connection: str + mesh_role: MeshRoles + wan_enabled: bool + + @dataclass class FritzRequireKeysMixin: """Fritz sensor data class.""" @@ -145,8 +154,7 @@ class FritzRequireKeysMixin: class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixin): """Describes Fritz sensor entity.""" - connection_type: Literal["dsl"] | None = None - exclude_mesh_role: MeshRoles = MeshRoles.SLAVE + is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( @@ -162,7 +170,7 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_device_uptime_state, - exclude_mesh_role=MeshRoles.NONE, + is_suitable=lambda info: True, ), FritzSensorEntityDescription( key="connection_uptime", @@ -225,7 +233,6 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", value_fn=_retrieve_link_kb_s_sent_state, - connection_type=DSL_CONNECTION, ), FritzSensorEntityDescription( key="link_kb_s_received", @@ -233,7 +240,6 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", value_fn=_retrieve_link_kb_s_received_state, - connection_type=DSL_CONNECTION, ), FritzSensorEntityDescription( key="link_noise_margin_sent", @@ -241,7 +247,7 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, icon="mdi:upload", value_fn=_retrieve_link_noise_margin_sent_state, - connection_type=DSL_CONNECTION, + is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), FritzSensorEntityDescription( key="link_noise_margin_received", @@ -249,7 +255,7 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, icon="mdi:download", value_fn=_retrieve_link_noise_margin_received_state, - connection_type=DSL_CONNECTION, + is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), FritzSensorEntityDescription( key="link_attenuation_sent", @@ -257,7 +263,7 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, icon="mdi:upload", value_fn=_retrieve_link_attenuation_sent_state, - connection_type=DSL_CONNECTION, + is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), FritzSensorEntityDescription( key="link_attenuation_received", @@ -265,7 +271,7 @@ class FritzSensorEntityDescription(SensorEntityDescription, FritzRequireKeysMixi native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, icon="mdi:download", value_fn=_retrieve_link_attenuation_received_state, - connection_type=DSL_CONNECTION, + is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION, ), ) @@ -278,19 +284,22 @@ async def async_setup_entry( avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] link_properties = await avm_wrapper.async_get_wan_link_properties() - dsl: bool = link_properties.get("NewWANAccessType") == "DSL" + connection_info = ConnectionInfo( + connection=link_properties.get("NewWANAccessType", "").lower(), + mesh_role=avm_wrapper.mesh_role, + wan_enabled=avm_wrapper.device_is_router, + ) _LOGGER.debug( - "WANAccessType of FritzBox %s is '%s'", + "ConnectionInfo for FritzBox %s: %s", avm_wrapper.host, - link_properties.get("NewWANAccessType"), + connection_info, ) entities = [ FritzBoxSensor(avm_wrapper, entry.title, description) for description in SENSOR_TYPES - if (dsl or description.connection_type != DSL_CONNECTION) - and description.exclude_mesh_role != avm_wrapper.mesh_role + if description.is_suitable(connection_info) ] async_add_entities(entities, True) diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index e32f4c7ffd721..c4e7de8df5e8d 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -1,6 +1,10 @@ """Services for Fritz integration.""" +from __future__ import annotations + import logging +import voluptuous as vol + from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError @@ -13,18 +17,31 @@ SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT, + SERVICE_SET_GUEST_WIFI_PW, ) _LOGGER = logging.getLogger(__name__) +SERVICE_SCHEMA_SET_GUEST_WIFI_PW = vol.Schema( + { + vol.Required("device_id"): str, + vol.Optional("password"): vol.Length(min=8, max=63), + vol.Optional("length"): vol.Range(min=8, max=63), + } +) -SERVICE_LIST = [SERVICE_CLEANUP, SERVICE_REBOOT, SERVICE_RECONNECT] +SERVICE_LIST: list[tuple[str, vol.Schema | None]] = [ + (SERVICE_CLEANUP, None), + (SERVICE_REBOOT, None), + (SERVICE_RECONNECT, None), + (SERVICE_SET_GUEST_WIFI_PW, SERVICE_SCHEMA_SET_GUEST_WIFI_PW), +] async def async_setup_services(hass: HomeAssistant) -> None: """Set up services for Fritz integration.""" - for service in SERVICE_LIST: + for service, _ in SERVICE_LIST: if hass.services.has_service(DOMAIN, service): return @@ -51,8 +68,8 @@ async def async_call_fritz_service(service_call: ServiceCall) -> None: service_call.service, ) - for service in SERVICE_LIST: - hass.services.async_register(DOMAIN, service, async_call_fritz_service) + for service, schema in SERVICE_LIST: + hass.services.async_register(DOMAIN, service, async_call_fritz_service, schema) async def _async_get_configured_avm_device( @@ -80,5 +97,5 @@ async def async_unload_services(hass: HomeAssistant) -> None: hass.data[FRITZ_SERVICES] = False - for service in SERVICE_LIST: + for service, _ in SERVICE_LIST: hass.services.async_remove(DOMAIN, service) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 2375aa71f575a..3c7ed6438417a 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -35,3 +35,30 @@ cleanup: integration: fritz entity: device_class: connectivity +set_guest_wifi_password: + name: Set guest wifi password + description: Set a new password for the guest wifi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters. + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to check + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity + password: + name: Password + description: New password for the guest wifi + required: false + selector: + text: + length: + name: Password length + description: Length of the new password. The password will be auto-generated, if no password is set. + required: false + selector: + number: + min: 8 + max: 63 diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index f1cdb71974133..450566f101b48 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -45,7 +45,8 @@ "step": { "init": { "data": { - "consider_home": "Seconds to consider a device at 'home'" + "consider_home": "Seconds to consider a device at 'home'", + "old_discovery": "Enable old discovery method" } } } diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index daddae5720cfd..730ffb7fc0d76 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -31,6 +31,7 @@ SWITCH_TYPE_DEFLECTION, SWITCH_TYPE_PORTFORWARD, SWITCH_TYPE_WIFINETWORK, + WIFI_STANDARD, MeshRoles, ) @@ -59,9 +60,7 @@ def deflection_entities_list( _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] - deflection_list = avm_wrapper.get_ontel_deflections() - - if not deflection_list: + if not (deflection_list := avm_wrapper.get_ontel_deflections()): return [] items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] @@ -141,34 +140,43 @@ def wifi_entities_list( ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_WIFINETWORK) - std_table = {"ax": "Wifi6", "ac": "5Ghz", "n": "2.4Ghz"} - if avm_wrapper.model == "FRITZ!Box 7390": - std_table = {"n": "5Ghz"} + # + # https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/wlanconfigSCPD.pdf + # + wifi_count = len( + [ + s + for s in avm_wrapper.connection.services + if s.startswith("WLANConfiguration") + ] + ) + _LOGGER.debug("WiFi networks count: %s", wifi_count) networks: dict = {} - for i in range(4): - if not ("WLANConfiguration" + str(i)) in avm_wrapper.connection.services: - continue - - network_info = avm_wrapper.get_wlan_configuration(i) - if network_info: - ssid = network_info["NewSSID"] - _LOGGER.debug("SSID from device: <%s>", ssid) - if ( - slugify( - ssid, - ) - in [slugify(v) for v in networks.values()] - ): - _LOGGER.debug("SSID duplicated, adding suffix") - networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' - else: - networks[i] = ssid - _LOGGER.debug("SSID normalized: <%s>", networks[i]) - + for i in range(1, wifi_count + 1): + network_info = avm_wrapper.connection.call_action( + f"WLANConfiguration{i}", "GetInfo" + ) + # Devices with 4 WLAN services, use the 2nd for internal communications + if not (wifi_count == 4 and i == 2): + networks[i] = { + "ssid": network_info["NewSSID"], + "bssid": network_info["NewBSSID"], + "standard": network_info["NewStandard"], + "enabled": network_info["NewEnable"], + "status": network_info["NewStatus"], + } + for i, network in networks.copy().items(): + networks[i]["switch_name"] = network["ssid"] + if len([j for j, n in networks.items() if n["ssid"] == network["ssid"]]) > 1: + networks[i]["switch_name"] += f" ({WIFI_STANDARD[i]})" + + _LOGGER.debug("WiFi networks list: %s", networks) return [ - FritzBoxWifiSwitch(avm_wrapper, device_friendly_name, net, network_name) - for net, network_name in networks.items() + FritzBoxWifiSwitch( + avm_wrapper, device_friendly_name, index, data["switch_name"] + ) + for index, data in networks.items() ] @@ -471,6 +479,17 @@ def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: self._name = f"{device.hostname} Internet Access" self._attr_unique_id = f"{self._mac}_internet_access" self._attr_entity_category = EntityCategory.CONFIG + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + default_name=device.hostname, + identifiers={(DOMAIN, self._mac)}, + via_device=( + DOMAIN, + avm_wrapper.unique_id, + ), + ) @property def is_on(self) -> bool | None: @@ -484,21 +503,6 @@ def available(self) -> bool: return False return super().available - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, self._mac)}, - default_manufacturer="AVM", - default_model="FRITZ!Box Tracked device", - default_name=self.name, - identifiers={(DOMAIN, self._mac)}, - via_device=( - DOMAIN, - self._avm_wrapper.unique_id, - ), - ) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self._async_handle_turn_on_off(turn_on=True) diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 75ad51d8a1eb0..9afc3d8553632 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -31,6 +31,11 @@ "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } + }, + "user": { + "data": { + "port": "Port" + } } } } diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 8ff8b1324c467..443f8b95c9359 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -1,20 +1,52 @@ { "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", + "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": "\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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "connection_error": "\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" + }, "flow_title": "{name}", "step": { "confirm": { + "data": { + "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": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf FRITZ!Box: {name} \n\n \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03bf\u03c5 {name}", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 FRITZ!Box Tools" }, "reauth_confirm": { + "data": { + "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": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c4\u03bf\u03c5 FRITZ!Box Tools \u03b3\u03b9\u03b1: {host} . \n\n \u03a4\u03bf FRITZ!Box Tools \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf FRITZ!Box \u03c3\u03b1\u03c2.", "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 FRITZ!Box Tools - \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1" }, "start_config": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b5 \u03c4\u03bf FRITZ!Box \u03c3\u03b1\u03c2.\n \u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf: \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 FRITZ!Box Tools - \u03c5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc" }, "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b5 \u03c4\u03bf FRITZ!Box \u03c3\u03b1\u03c2.\n \u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf: \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 FRITZ!Box Tools" } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 0fa47bd8328fa..0a58ee686f3ff 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,7 +9,6 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", - "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication" }, "flow_title": "{name}", @@ -30,16 +29,6 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, - "start_config": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", - "title": "Setup FRITZ!Box Tools - mandatory" - }, "user": { "data": { "host": "Host", @@ -56,7 +45,8 @@ "step": { "init": { "data": { - "consider_home": "Seconds to consider a device at 'home'" + "consider_home": "Seconds to consider a device at 'home'", + "old_discovery": "Enable old discovery method" } } } diff --git a/homeassistant/components/fritz/translations/nb.json b/homeassistant/components/fritz/translations/nb.json new file mode 100644 index 0000000000000..5e712fccbd983 --- /dev/null +++ b/homeassistant/components/fritz/translations/nb.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "Brukernavn" + } + }, + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + }, + "start_config": { + "data": { + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json new file mode 100644 index 0000000000000..a28f063ab6d65 --- /dev/null +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "connection_error": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Descoberto FRITZ!Box: {name} \n\n Configure as Ferramentas do FRITZ!Box para controlar o seu {name}", + "title": "Configurar as Ferramentas do FRITZ!Box" + }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Atualize as credenciais do FRITZ!Box Tools para: {host} . \n\n O FRITZ!Box Tools n\u00e3o consegue iniciar sess\u00e3o no seu FRITZ!Box.", + "title": "Atualizando as Ferramentas do FRITZ!Box - credenciais" + }, + "start_config": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "description": "Configure as Ferramentas do FRITZ!Box para controlar o seu FRITZ!Box.\n M\u00ednimo necess\u00e1rio: nome de usu\u00e1rio, senha.", + "title": "Configurar as Ferramentas do FRITZ!Box - obrigat\u00f3rio" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "description": "Configure as Ferramentas do FRITZ!Box para controlar o seu FRITZ!Box.\nM\u00ednimo necess\u00e1rio: nome de usu\u00e1rio, senha.", + "title": "Configurar as Ferramentas do FRITZ!Box" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Segundos para considerar um dispositivo em 'casa'" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/sk.json b/homeassistant/components/fritz/translations/sk.json new file mode 100644 index 0000000000000..9d83bc4b75642 --- /dev/null +++ b/homeassistant/components/fritz/translations/sk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "start_config": { + "data": { + "port": "Port" + } + }, + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 98c02d0166ed2..1dac4ddd78a5e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.6.2"], + "requirements": ["pyfritzhome==0.6.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" @@ -10,5 +10,6 @@ ], "codeowners": ["@mib1185", "@flabbamann"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyfritzhome"] } diff --git a/homeassistant/components/fritzbox/translations/el.json b/homeassistant/components/fritzbox/translations/el.json index c5524f1079043..975126772d56d 100644 --- a/homeassistant/components/fritzbox/translations/el.json +++ b/homeassistant/components/fritzbox/translations/el.json @@ -1,12 +1,38 @@ { "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": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf AVM FRITZ!Box \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Smart Home.", + "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_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" + }, "flow_title": "{name}", "step": { "confirm": { + "data": { + "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": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "reauth_confirm": { + "data": { + "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": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {name}." + }, + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/pt-BR.json b/homeassistant/components/fritzbox/translations/pt-BR.json index 9685e93f92710..3e3884cbc41cb 100644 --- a/homeassistant/components/fritzbox/translations/pt-BR.json +++ b/homeassistant/components/fritzbox/translations/pt-BR.json @@ -1,5 +1,16 @@ { "config": { + "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", + "not_supported": "Conectado ao AVM FRITZ!Box, mas n\u00e3o consegue controlar os dispositivos Smart Home.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -8,11 +19,20 @@ }, "description": "Voc\u00ea quer configurar o {name}?" }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Atualize suas informa\u00e7\u00f5es de login para {name}." + }, "user": { "data": { + "host": "Nome do host", "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "description": "Insira as informa\u00e7\u00f5es do seu AVM FRITZ!Box" } } } diff --git a/homeassistant/components/fritzbox/translations/sk.json b/homeassistant/components/fritzbox/translations/sk.json new file mode 100644 index 0000000000000..e0e6b1c5bda91 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index b28d76a71cc85..a33e01153b7cf 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": ["fritzconnection==1.8.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["fritzconnection"] } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 490b67c03bd8f..5efb0776c418f 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -55,6 +55,7 @@ SCAN_INTERVAL = timedelta(hours=3) +# Deprecated in Home Assistant 2022.3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, @@ -75,6 +76,12 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Import the platform into a config entry.""" + _LOGGER.warning( + "Configuration of the AVM FRITZ!Box Call Monitor sensor platform in YAML " + "is deprecated and will be removed in Home Assistant 2022.5; " + "Your existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config diff --git a/homeassistant/components/fritzbox_callmonitor/translations/el.json b/homeassistant/components/fritzbox_callmonitor/translations/el.json new file mode 100644 index 0000000000000..7f16643e4f8f0 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/el.json @@ -0,0 +1,41 @@ +{ + "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", + "insufficient_permissions": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03b1\u03c1\u03ba\u03ae \u03b4\u03b9\u03ba\u03b1\u03b9\u03ce\u03bc\u03b1\u03c4\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03c5 AVM FRITZ!Box \u03ba\u03b1\u03b9 \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03b9\u03ba\u03bf\u03cd\u03c2 \u03ba\u03b1\u03c4\u03b1\u03bb\u03cc\u03b3\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5.", + "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": { + "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" + }, + "flow_title": "{name}", + "step": { + "phonebook": { + "data": { + "phonebook": "\u03a4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03b9\u03ba\u03cc\u03c2 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2" + } + }, + "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u03a4\u03b1 \u03c0\u03c1\u03bf\u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae \u03c4\u03bf\u03c5\u03c2." + }, + "step": { + "init": { + "data": { + "prefixes": "\u03a0\u03c1\u03bf\u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 (\u03bb\u03af\u03c3\u03c4\u03b1 \u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1)" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b8\u03b5\u03bc\u03ac\u03c4\u03c9\u03bd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pt-BR.json b/homeassistant/components/fritzbox_callmonitor/translations/pt-BR.json new file mode 100644 index 0000000000000..5591951539738 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "insufficient_permissions": "O usu\u00e1rio n\u00e3o tem permiss\u00f5es suficientes para acessar as configura\u00e7\u00f5es do AVM FRITZ!Box e suas listas telef\u00f4nicas.", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Lista telef\u00f4nica" + } + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Os prefixos est\u00e3o malformados, verifique sua formata\u00e7\u00e3o." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefixos (lista separada por v\u00edrgulas)" + }, + "title": "Configurar prefixos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/sk.json b/homeassistant/components/fritzbox_callmonitor/translations/sk.json new file mode 100644 index 0000000000000..1145b3bb9f844 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index d2f3fc2e0f351..f78489a2ea1c6 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -11,5 +11,6 @@ "iot_class": "local_polling", "name": "Fronius", "quality_scale": "platinum", - "requirements": ["pyfronius==0.7.1"] + "requirements": ["pyfronius==0.7.1"], + "loggers": ["pyfronius"] } diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 2674778583773..a266998a9b5f0 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -24,6 +24,7 @@ FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, + POWER_VOLT_AMPERE_REACTIVE, POWER_WATT, TEMP_CELSIUS, ) @@ -52,7 +53,6 @@ ELECTRIC_CHARGE_AMPERE_HOURS: Final = "Ah" ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" -POWER_VOLT_AMPERE_REACTIVE: Final = "var" PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( @@ -338,6 +338,7 @@ async def async_setup_entry( key="power_apparent_phase_1", name="Power apparent phase 1", native_unit_of_measurement=POWER_VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -346,6 +347,7 @@ async def async_setup_entry( key="power_apparent_phase_2", name="Power apparent phase 2", native_unit_of_measurement=POWER_VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -354,6 +356,7 @@ async def async_setup_entry( key="power_apparent_phase_3", name="Power apparent phase 3", native_unit_of_measurement=POWER_VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -362,6 +365,7 @@ async def async_setup_entry( key="power_apparent", name="Power apparent", native_unit_of_measurement=POWER_VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -397,6 +401,7 @@ async def async_setup_entry( key="power_reactive_phase_1", name="Power reactive phase 1", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + device_class=SensorDeviceClass.REACTIVE_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -405,6 +410,7 @@ async def async_setup_entry( key="power_reactive_phase_2", name="Power reactive phase 2", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + device_class=SensorDeviceClass.REACTIVE_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -413,6 +419,7 @@ async def async_setup_entry( key="power_reactive_phase_3", name="Power reactive phase 3", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + device_class=SensorDeviceClass.REACTIVE_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, @@ -421,6 +428,7 @@ async def async_setup_entry( key="power_reactive", name="Power reactive", native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE, + device_class=SensorDeviceClass.REACTIVE_POWER, state_class=SensorStateClass.MEASUREMENT, icon="mdi:flash-outline", entity_registry_enabled_default=False, diff --git a/homeassistant/components/fronius/translations/cs.json b/homeassistant/components/fronius/translations/cs.json index 4fcafe6fcced0..773ee67a7cf50 100644 --- a/homeassistant/components/fronius/translations/cs.json +++ b/homeassistant/components/fronius/translations/cs.json @@ -3,6 +3,9 @@ "abort": { "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa" }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "flow_title": "{device}" } } \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/el.json b/homeassistant/components/fronius/translations/el.json index 7e137fac7599b..197838ae7dc57 100644 --- a/homeassistant/components/fronius/translations/el.json +++ b/homeassistant/components/fronius/translations/el.json @@ -1,9 +1,24 @@ { "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", + "invalid_host": "\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" + }, + "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" + }, "flow_title": "{device}", "step": { "confirm_discovery": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device} \u03c3\u03c4\u03bf Home Assistant;" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \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 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Fronius.", + "title": "Fronius SolarNet" } } } diff --git a/homeassistant/components/fronius/translations/nb.json b/homeassistant/components/fronius/translations/nb.json new file mode 100644 index 0000000000000..89900954d12ae --- /dev/null +++ b/homeassistant/components/fronius/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "invalid_host": "Ugyldig vertsnavn eller IP-adresse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/pt-BR.json b/homeassistant/components/fronius/translations/pt-BR.json new file mode 100644 index 0000000000000..da83db69451f0 --- /dev/null +++ b/homeassistant/components/fronius/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{device}", + "step": { + "confirm_discovery": { + "description": "Deseja adicionar {device} ao Home Assistant?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure o endere\u00e7o IP ou nome de host local do seu dispositivo Fronius.", + "title": "Fronius SolarNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c6812f4d9dec2..803b093fd406f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -7,7 +7,7 @@ import logging import os import pathlib -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from aiohttp import hdrs, web, web_urldispatcher import jinja2 @@ -313,7 +313,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: # pylint: disable=import-outside-toplevel import hass_frontend - return cast(pathlib.Path, hass_frontend.where()) + return hass_frontend.where() async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 36f55df0932f6..cc118e23dc91f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220203.1" + "home-assistant-frontend==20220301.0" ], "dependencies": [ "api", @@ -13,8 +13,7 @@ "diagnostics", "http", "lovelace", - "onboarding", - "search", + "onboarding", "search", "system_log", "websocket_api" ], diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json new file mode 100644 index 0000000000000..3b814303e6958 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/el.json b/homeassistant/components/garages_amsterdam/translations/el.json index da926cb501727..3c3da7695e1db 100644 --- a/homeassistant/components/garages_amsterdam/translations/el.json +++ b/homeassistant/components/garages_amsterdam/translations/el.json @@ -1,5 +1,10 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "user": { "data": { @@ -8,5 +13,6 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b3\u03ba\u03b1\u03c1\u03ac\u03b6 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" } } - } + }, + "title": "Garages Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/pt-BR.json b/homeassistant/components/garages_amsterdam/translations/pt-BR.json new file mode 100644 index 0000000000000..ea66c718a27f1 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "garage_name": "Nome da garagem" + }, + "title": "Escolha uma garagem para monitorar" + } + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/el.json b/homeassistant/components/gdacs/translations/el.json index 44917a2ca182f..dcedef41f8c94 100644 --- a/homeassistant/components/gdacs/translations/el.json +++ b/homeassistant/components/gdacs/translations/el.json @@ -1,10 +1,14 @@ { "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" + }, "step": { "user": { "data": { "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1" - } + }, + "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c6\u03af\u03bb\u03c4\u03c1\u03bf\u03c5 \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/gdacs/translations/pt-BR.json b/homeassistant/components/gdacs/translations/pt-BR.json index 1e866fa8059c3..53de0312b7384 100644 --- a/homeassistant/components/gdacs/translations/pt-BR.json +++ b/homeassistant/components/gdacs/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Local j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 38a0ff15af350..83e358acc34a5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,7 +1,7 @@ """Support for a Genius Hub system.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import Any @@ -223,7 +223,7 @@ class GeniusEntity(Entity): def __init__(self) -> None: """Initialize the entity.""" - self._unique_id = self._name = None + self._unique_id: str | None = None async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" @@ -238,11 +238,6 @@ def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id - @property - def name(self) -> str: - """Return the name of the geniushub entity.""" - return self._name - @property def should_poll(self) -> bool: """Return False as geniushub entities should not be polled.""" @@ -258,7 +253,8 @@ def __init__(self, broker, device) -> None: self._device = device self._unique_id = f"{broker.hub_uid}_device_{device.id}" - self._last_comms = self._state_attr = None + self._last_comms: datetime | None = None + self._state_attr = None @property def extra_state_attributes(self) -> dict[str, Any]: @@ -337,11 +333,9 @@ def extra_state_attributes(self) -> dict[str, Any]: class GeniusHeatingZone(GeniusZone): """Base for Genius Heating Zones.""" - def __init__(self, broker, zone) -> None: - """Initialize the Zone.""" - super().__init__(broker, zone) - - self._max_temp = self._min_temp = self._supported_features = None + _max_temp: float + _min_temp: float + _supported_features: int @property def current_temperature(self) -> float | None: diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 1e54df528204e..f00019361e527 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -42,9 +42,9 @@ def __init__(self, broker, device, state_attr) -> None: self._state_attr = state_attr if device.type[:21] == "Dual Channel Receiver": - self._name = f"{device.type[:21]} {device.id}" + self._attr_name = f"{device.type[:21]} {device.id}" else: - self._name = f"{device.type} {device.id}" + self._attr_name = f"{device.type} {device.id}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 698da72c3f461..9d5bc7f932885 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": ["geniushub-client==0.6.30"], "codeowners": ["@zxdavb"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["geniushubclient"] } diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index fad66c493129c..20acb533a2c7a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -34,14 +34,14 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] - sensors = [ + entities: list[GeniusBattery | GeniusIssue] = [ GeniusBattery(broker, d, GH_STATE_ATTR) for d in broker.client.device_objs if GH_STATE_ATTR in d.data["state"] ] - issues = [GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)] + entities.extend([GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)]) - async_add_entities(sensors + issues, update_before_add=True) + async_add_entities(entities, update_before_add=True) class GeniusBattery(GeniusDevice, SensorEntity): @@ -53,7 +53,7 @@ def __init__(self, broker, device, state_attr) -> None: self._state_attr = state_attr - self._name = f"{device.type} {device.id}" + self._attr_name = f"{device.type} {device.id}" @property def icon(self) -> str: @@ -62,7 +62,10 @@ def icon(self) -> str: interval = timedelta( seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60) ) - if self._last_comms < dt_util.utcnow() - interval * 3: + if ( + not self._last_comms + or self._last_comms < dt_util.utcnow() - interval * 3 + ): return "mdi:battery-unknown" battery_level = self._device.data["state"][self._state_attr] @@ -104,12 +107,12 @@ def __init__(self, broker, level) -> None: self._hub = broker.client self._unique_id = f"{broker.hub_uid}_{GH_LEVEL_MAPPING[level]}" - self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" + self._attr_name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level - self._issues = [] + self._issues: list = [] @property - def native_value(self) -> str: + def native_value(self) -> int: """Return the number of issues.""" return len(self._issues) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index f1ff8444d28d3..a64aa1345e105 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -73,7 +73,7 @@ def operation_list(self) -> list[str]: @property def current_operation(self) -> str: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._zone.data["mode"]] + return GH_STATE_TO_HA[self._zone.data["mode"]] # type: ignore[return-value] async def async_set_operation_mode(self, operation_mode) -> None: """Set a new operation mode for this boiler.""" diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index aba5abff67c3a..8f54c81664923 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": ["geojson_client==0.6"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geojson_client"] } diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 6a470e1ddbdbb..30dd4c5af50b3 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": ["georss_generic_client==0.6"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["georss_client", "georss_generic_client"] } diff --git a/homeassistant/components/geofency/translations/bg.json b/homeassistant/components/geofency/translations/bg.json index 916336f37a40b..eaecf88293ddf 100644 --- a/homeassistant/components/geofency/translations/bg.json +++ b/homeassistant/components/geofency/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/geofency/translations/ca.json b/homeassistant/components/geofency/translations/ca.json index a956bf5ce62aa..d2bb9582a60ac 100644 --- a/homeassistant/components/geofency/translations/ca.json +++ b/homeassistant/components/geofency/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/geofency/translations/de.json b/homeassistant/components/geofency/translations/de.json index 9c3fd3ea1b0de..b9b06cfd02016 100644 --- a/homeassistant/components/geofency/translations/de.json +++ b/homeassistant/components/geofency/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/geofency/translations/el.json b/homeassistant/components/geofency/translations/el.json index 5252249c79bb8..cf51a439e06f3 100644 --- a/homeassistant/components/geofency/translations/el.json +++ b/homeassistant/components/geofency/translations/el.json @@ -1,10 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Geofency.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Webhook Geofency;", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Geofency Webhook" + } } } } \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/en.json b/homeassistant/components/geofency/translations/en.json index db7a9e684f933..5b97afb3bcc2b 100644 --- a/homeassistant/components/geofency/translations/en.json +++ b/homeassistant/components/geofency/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/geofency/translations/et.json b/homeassistant/components/geofency/translations/et.json index fa87d80871fdf..4283bcb83b1b0 100644 --- a/homeassistant/components/geofency/translations/et.json +++ b/homeassistant/components/geofency/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/geofency/translations/fr.json b/homeassistant/components/geofency/translations/fr.json index 270a627579128..db163efeac712 100644 --- a/homeassistant/components/geofency/translations/fr.json +++ b/homeassistant/components/geofency/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/geofency/translations/he.json b/homeassistant/components/geofency/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/geofency/translations/he.json +++ b/homeassistant/components/geofency/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index 1b3f17fe700cb..9da5f3622b6ee 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/geofency/translations/id.json b/homeassistant/components/geofency/translations/id.json index 0e5163b96cd5c..9793131eb4c43 100644 --- a/homeassistant/components/geofency/translations/id.json +++ b/homeassistant/components/geofency/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/geofency/translations/it.json b/homeassistant/components/geofency/translations/it.json index a72adf25c5017..07647dc6df321 100644 --- a/homeassistant/components/geofency/translations/it.json +++ b/homeassistant/components/geofency/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index ba80a1979948a..e653cb99d430c 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/geofency/translations/nb.json b/homeassistant/components/geofency/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/geofency/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/nl.json b/homeassistant/components/geofency/translations/nl.json index 59ed1cf6b5bf1..30a2acfdaddc8 100644 --- a/homeassistant/components/geofency/translations/nl.json +++ b/homeassistant/components/geofency/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/geofency/translations/no.json b/homeassistant/components/geofency/translations/no.json index 51fa75f66b442..1ccd77230b60e 100644 --- a/homeassistant/components/geofency/translations/no.json +++ b/homeassistant/components/geofency/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/geofency/translations/pl.json b/homeassistant/components/geofency/translations/pl.json index c504e31051a99..109c0b58c7096 100644 --- a/homeassistant/components/geofency/translations/pl.json +++ b/homeassistant/components/geofency/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/geofency/translations/pt-BR.json b/homeassistant/components/geofency/translations/pt-BR.json index d8b430df5f430..226c388532e10 100644 --- a/homeassistant/components/geofency/translations/pt-BR.json +++ b/homeassistant/components/geofency/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." }, diff --git a/homeassistant/components/geofency/translations/ru.json b/homeassistant/components/geofency/translations/ru.json index c4ac5e7928275..a0a79f1ae3a2a 100644 --- a/homeassistant/components/geofency/translations/ru.json +++ b/homeassistant/components/geofency/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json index 4cd04c64d7b3e..8cd04ad16c79f 100644 --- a/homeassistant/components/geofency/translations/tr.json +++ b/homeassistant/components/geofency/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/geofency/translations/uk.json b/homeassistant/components/geofency/translations/uk.json index 54a14afb764d9..b38d3b66c7cd1 100644 --- a/homeassistant/components/geofency/translations/uk.json +++ b/homeassistant/components/geofency/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/geofency/translations/zh-Hans.json b/homeassistant/components/geofency/translations/zh-Hans.json index d6355fd3809e8..9a04764873cb7 100644 --- a/homeassistant/components/geofency/translations/zh-Hans.json +++ b/homeassistant/components/geofency/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e Geofency \u7684 Webhook \u529f\u80fd\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, diff --git a/homeassistant/components/geofency/translations/zh-Hant.json b/homeassistant/components/geofency/translations/zh-Hant.json index 4ffe673045388..6862ab5208e64 100644 --- a/homeassistant/components/geofency/translations/zh-Hant.json +++ b/homeassistant/components/geofency/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 5668cd6cb3f06..ba8eecc4ae920 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aio_geojson_geonetnz_quakes==0.13"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aio_geojson_geonetnz_quakes"] } diff --git a/homeassistant/components/geonetnz_quakes/translations/el.json b/homeassistant/components/geonetnz_quakes/translations/el.json index 215801cc7c588..be08ac36a4711 100644 --- a/homeassistant/components/geonetnz_quakes/translations/el.json +++ b/homeassistant/components/geonetnz_quakes/translations/el.json @@ -1,8 +1,12 @@ { "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" + }, "step": { "user": { "data": { + "mmi": "MMI", "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1" }, "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c6\u03af\u03bb\u03c4\u03c1\u03bf\u03c5 \u03c3\u03b1\u03c2." diff --git a/homeassistant/components/geonetnz_quakes/translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/translations/pt-BR.json index ee705850b0319..60cc25c59ce8a 100644 --- a/homeassistant/components/geonetnz_quakes/translations/pt-BR.json +++ b/homeassistant/components/geonetnz_quakes/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Local j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index dbd793c49b336..a365237561afe 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", "requirements": ["aio_geojson_geonetnz_volcano==0.6"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aio_geojson_geonetnz_volcano"] } diff --git a/homeassistant/components/geonetnz_volcano/translations/el.json b/homeassistant/components/geonetnz_volcano/translations/el.json index 82e2fb54adce5..23fbdf2cc0da5 100644 --- a/homeassistant/components/geonetnz_volcano/translations/el.json +++ b/homeassistant/components/geonetnz_volcano/translations/el.json @@ -1,7 +1,13 @@ { "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" + }, "step": { "user": { + "data": { + "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1" + }, "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c6\u03af\u03bb\u03c4\u03c1\u03bf\u03c5 \u03c3\u03b1\u03c2." } } diff --git a/homeassistant/components/geonetnz_volcano/translations/pt-BR.json b/homeassistant/components/geonetnz_volcano/translations/pt-BR.json index 98180e11248aa..5faa4ea054a91 100644 --- a/homeassistant/components/geonetnz_volcano/translations/pt-BR.json +++ b/homeassistant/components/geonetnz_volcano/translations/pt-BR.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { "radius": "Raio" - } + }, + "title": "Preencha os dados do filtro." } } } diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 0e7227797d2b2..20ad912d40cce 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -6,5 +6,6 @@ "requirements": ["gios==2.1.0"], "config_flow": true, "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["dacite", "gios"] } diff --git a/homeassistant/components/gios/translations/el.json b/homeassistant/components/gios/translations/el.json index c661393640149..ae916d7066797 100644 --- a/homeassistant/components/gios/translations/el.json +++ b/homeassistant/components/gios/translations/el.json @@ -1,13 +1,27 @@ { "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_sensors_data": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2.", + "wrong_station_id": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc." + }, "step": { "user": { "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "station_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03ad\u03c1\u03b1 GIO\u015a (\u03a0\u03bf\u03bb\u03c9\u03bd\u03b9\u03ba\u03ae \u0395\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03ae\u03c2 \u0395\u03c0\u03b9\u03b8\u03b5\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03a0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd\u03c4\u03bf\u03c2). \u0395\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03c1\u03af\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b1\u03c4\u03b9\u03ac \u03b5\u03b4\u03ce: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (\u03a0\u03bf\u03bb\u03c9\u03bd\u03b9\u03ba\u03ae \u0393\u03b5\u03bd\u03b9\u03ba\u03ae \u0395\u03c0\u03b9\u03b8\u03b5\u03ce\u03c1\u03b7\u03c3\u03b7 \u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1\u03c2 \u03a0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd\u03c4\u03bf\u03c2)" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/pt-BR.json b/homeassistant/components/gios/translations/pt-BR.json index 83add749e4710..39472c3b42009 100644 --- a/homeassistant/components/gios/translations/pt-BR.json +++ b/homeassistant/components/gios/translations/pt-BR.json @@ -1,9 +1,27 @@ { "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_sensors_data": "Dados de sensores inv\u00e1lidos para esta esta\u00e7\u00e3o de medi\u00e7\u00e3o.", + "wrong_station_id": "O ID da esta\u00e7\u00e3o de medi\u00e7\u00e3o n\u00e3o est\u00e1 correto." + }, "step": { "user": { + "data": { + "name": "Nome", + "station_id": "ID da esta\u00e7\u00e3o de medi\u00e7\u00e3o" + }, + "description": "Configurar a integra\u00e7\u00e3o da qualidade do ar GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Se precisar de ajuda com a configura\u00e7\u00e3o, d\u00ea uma olhada em https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Inspetor-Chefe Polon\u00eas de Prote\u00e7\u00e3o Ambiental)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcance o servidor GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/sk.json b/homeassistant/components/gios/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/gios/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index ae13e8df9590e..4ecff6e9648b8 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -13,13 +13,7 @@ ) from .const import CONF_REPOSITORIES, DOMAIN, LOGGER -from .coordinator import ( - DataUpdateCoordinators, - RepositoryCommitDataUpdateCoordinator, - RepositoryInformationDataUpdateCoordinator, - RepositoryIssueDataUpdateCoordinator, - RepositoryReleaseDataUpdateCoordinator, -) +from .coordinator import GitHubDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -37,24 +31,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: repositories: list[str] = entry.options[CONF_REPOSITORIES] for repository in repositories: - coordinators: DataUpdateCoordinators = { - "information": RepositoryInformationDataUpdateCoordinator( - hass=hass, entry=entry, client=client, repository=repository - ), - "release": RepositoryReleaseDataUpdateCoordinator( - hass=hass, entry=entry, client=client, repository=repository - ), - "issue": RepositoryIssueDataUpdateCoordinator( - hass=hass, entry=entry, client=client, repository=repository - ), - "commit": RepositoryCommitDataUpdateCoordinator( - hass=hass, entry=entry, client=client, repository=repository - ), - } - - await coordinators["information"].async_config_entry_first_refresh() - - hass.data[DOMAIN][repository] = coordinators + coordinator = GitHubDataUpdateCoordinator( + hass=hass, + client=client, + repository=repository, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][repository] = coordinator async_cleanup_device_registry(hass=hass, entry=entry) diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index 7a0c471ab036f..efe9d7baa5e62 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -3,9 +3,6 @@ from datetime import timedelta from logging import Logger, getLogger -from typing import NamedTuple - -from aiogithubapi import GitHubIssueModel LOGGER: Logger = getLogger(__package__) @@ -18,12 +15,3 @@ CONF_ACCESS_TOKEN = "access_token" CONF_REPOSITORIES = "repositories" - - -class IssuesPulls(NamedTuple): - """Issues and pull requests.""" - - issues_count: int - issue_last: GitHubIssueModel | None - pulls_count: int - pull_last: GitHubIssueModel | None diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 32f0df98e1f46..10f30bb100638 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -1,44 +1,115 @@ -"""Custom data update coordinators for the GitHub integration.""" +"""Custom data update coordinator for the GitHub integration.""" from __future__ import annotations -from typing import Literal, TypedDict +from typing import Any from aiogithubapi import ( GitHubAPI, - GitHubCommitModel, GitHubConnectionException, GitHubException, - GitHubNotModifiedException, GitHubRatelimitException, - GitHubReleaseModel, - GitHubRepositoryModel, GitHubResponseModel, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, T +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER, IssuesPulls - -CoordinatorKeyType = Literal["information", "release", "issue", "commit"] - - -class GitHubBaseDataUpdateCoordinator(DataUpdateCoordinator[T]): - """Base class for GitHub data update coordinators.""" +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER + +GRAPHQL_REPOSITORY_QUERY = """ +query ($owner: String!, $repository: String!) { + rateLimit { + cost + remaining + } + repository(owner: $owner, name: $repository) { + default_branch_ref: defaultBranchRef { + commit: target { + ... on Commit { + message: messageHeadline + url + sha: oid + } + } + } + stargazers_count: stargazerCount + forks_count: forkCount + full_name: nameWithOwner + id: databaseId + watchers(first: 1) { + total: totalCount + } + discussion: discussions( + first: 1 + orderBy: {field: CREATED_AT, direction: DESC} + ) { + total: totalCount + discussions: nodes { + title + url + number + } + } + issue: issues( + first: 1 + states: OPEN + orderBy: {field: CREATED_AT, direction: DESC} + ) { + total: totalCount + issues: nodes { + title + url + number + } + } + pull_request: pullRequests( + first: 1 + states: OPEN + orderBy: {field: CREATED_AT, direction: DESC} + ) { + total: totalCount + pull_requests: nodes { + title + url + number + } + } + release: latestRelease { + name + url + tag: tagName + } + refs( + first: 1 + refPrefix: "refs/tags/" + orderBy: {field: TAG_COMMIT_DATE, direction: DESC} + ) { + tags: nodes { + name + target { + url: commitUrl + } + } + } + } +} +""" + + +class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Data update coordinator for the GitHub integration.""" def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, client: GitHubAPI, repository: str, ) -> None: """Initialize GitHub data update coordinator base class.""" - self.config_entry = entry self.repository = repository self._client = client - self._last_response: GitHubResponseModel[T] | None = None + self._last_response: GitHubResponseModel[dict[str, Any]] | None = None + self.data = {} super().__init__( hass, @@ -47,30 +118,14 @@ def __init__( update_interval=DEFAULT_UPDATE_INTERVAL, ) - @property - def _etag(self) -> str: - """Return the ETag of the last response.""" - return self._last_response.etag if self._last_response is not None else None - - async def fetch_data(self) -> GitHubResponseModel[T]: - """Fetch data from GitHub API.""" - - @staticmethod - def _parse_response(response: GitHubResponseModel[T]) -> T: - """Parse the response from GitHub API.""" - return response.data - - async def _async_update_data(self) -> T: + async def _async_update_data(self) -> GitHubResponseModel[dict[str, Any]]: + """Update data.""" + owner, repository = self.repository.split("/") try: - response = await self.fetch_data() - except GitHubNotModifiedException: - LOGGER.debug( - "Content for %s with %s not modified", - self.repository, - self.__class__.__name__, + response = await self._client.graphql( + query=GRAPHQL_REPOSITORY_QUERY, + variables={"owner": owner, "repository": repository}, ) - # Return the last known data if the request result was not modified - return self.data except (GitHubConnectionException, GitHubRatelimitException) as exception: # These are expected and we dont log anything extra raise UpdateFailed(exception) from exception @@ -80,133 +135,4 @@ async def _async_update_data(self) -> T: raise UpdateFailed(exception) from exception else: self._last_response = response - return self._parse_response(response) - - -class RepositoryInformationDataUpdateCoordinator( - GitHubBaseDataUpdateCoordinator[GitHubRepositoryModel] -): - """Data update coordinator for repository information.""" - - async def fetch_data(self) -> GitHubResponseModel[GitHubRepositoryModel]: - """Get the latest data from GitHub.""" - return await self._client.repos.get(self.repository, **{"etag": self._etag}) - - -class RepositoryReleaseDataUpdateCoordinator( - GitHubBaseDataUpdateCoordinator[GitHubReleaseModel] -): - """Data update coordinator for repository release.""" - - @staticmethod - def _parse_response( - response: GitHubResponseModel[GitHubReleaseModel | None], - ) -> GitHubReleaseModel | None: - """Parse the response from GitHub API.""" - if not response.data: - return None - - for release in response.data: - if not release.prerelease and not release.draft: - return release - - # Fall back to the latest release if no non-prerelease release is found - return response.data[0] - - async def fetch_data(self) -> GitHubReleaseModel | None: - """Get the latest data from GitHub.""" - return await self._client.repos.releases.list( - self.repository, **{"etag": self._etag} - ) - - -class RepositoryIssueDataUpdateCoordinator( - GitHubBaseDataUpdateCoordinator[IssuesPulls] -): - """Data update coordinator for repository issues.""" - - _issue_etag: str | None = None - _pull_etag: str | None = None - - @staticmethod - def _parse_response(response: IssuesPulls) -> IssuesPulls: - """Parse the response from GitHub API.""" - return response - - async def fetch_data(self) -> IssuesPulls: - """Get the latest data from GitHub.""" - pulls_count = 0 - pull_last = None - issues_count = 0 - issue_last = None - try: - pull_response = await self._client.repos.pulls.list( - self.repository, - **{"params": {"per_page": 1}, "etag": self._pull_etag}, - ) - except GitHubNotModifiedException: - # Return the last known data if the request result was not modified - pulls_count = self.data.pulls_count - pull_last = self.data.pull_last - else: - self._pull_etag = pull_response.etag - pulls_count = pull_response.last_page_number or len(pull_response.data) - pull_last = pull_response.data[0] if pull_response.data else None - - try: - issue_response = await self._client.repos.issues.list( - self.repository, - **{"params": {"per_page": 1}, "etag": self._issue_etag}, - ) - except GitHubNotModifiedException: - # Return the last known data if the request result was not modified - issues_count = self.data.issues_count - issue_last = self.data.issue_last - else: - self._issue_etag = issue_response.etag - issues_count = ( - issue_response.last_page_number or len(issue_response.data) - ) - pulls_count - issue_last = issue_response.data[0] if issue_response.data else None - - if issue_last is not None and issue_last.pull_request: - issue_response = await self._client.repos.issues.list(self.repository) - for issue in issue_response.data: - if not issue.pull_request: - issue_last = issue - break - - return IssuesPulls( - issues_count=issues_count, - issue_last=issue_last, - pulls_count=pulls_count, - pull_last=pull_last, - ) - - -class RepositoryCommitDataUpdateCoordinator( - GitHubBaseDataUpdateCoordinator[GitHubCommitModel] -): - """Data update coordinator for repository commit.""" - - @staticmethod - def _parse_response( - response: GitHubResponseModel[GitHubCommitModel | None], - ) -> GitHubCommitModel | None: - """Parse the response from GitHub API.""" - return response.data[0] if response.data else None - - async def fetch_data(self) -> GitHubCommitModel | None: - """Get the latest data from GitHub.""" - return await self._client.repos.list_commits( - self.repository, **{"params": {"per_page": 1}, "etag": self._etag} - ) - - -class DataUpdateCoordinators(TypedDict): - """Custom data update coordinators for the GitHub integration.""" - - information: RepositoryInformationDataUpdateCoordinator - release: RepositoryReleaseDataUpdateCoordinator - issue: RepositoryIssueDataUpdateCoordinator - commit: RepositoryCommitDataUpdateCoordinator + return response.data["data"]["repository"] diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index 101bf642c91b9..c2546d636b828 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -13,7 +13,7 @@ ) from .const import CONF_ACCESS_TOKEN, DOMAIN -from .coordinator import DataUpdateCoordinators +from .coordinator import GitHubDataUpdateCoordinator async def async_get_config_entry_diagnostics( @@ -35,11 +35,10 @@ async def async_get_config_entry_diagnostics( else: data["rate_limit"] = rate_limit_response.data.as_dict - repositories: dict[str, DataUpdateCoordinators] = hass.data[DOMAIN] + repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] data["repositories"] = {} - for repository, coordinators in repositories.items(): - info = coordinators["information"].data - data["repositories"][repository] = info.as_dict if info else None + for repository, coordinator in repositories.items(): + data["repositories"][repository] = coordinator.data return data diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 8d63b27117e4c..196095a5b6e15 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,12 +3,15 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==22.2.0" + "aiogithubapi==22.2.3" ], "codeowners": [ "@timmo001", "@ludeeus" ], "iot_class": "cloud_polling", - "config_flow": true + "config_flow": true, + "loggers": [ + "aiogithubapi" + ] } \ No newline at end of file diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 18aaa43d18d5b..a09e440e2ce63 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -19,19 +19,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import ( - CoordinatorKeyType, - DataUpdateCoordinators, - GitHubBaseDataUpdateCoordinator, -) +from .coordinator import GitHubDataUpdateCoordinator @dataclass class BaseEntityDescriptionMixin: """Mixin for required GitHub base description keys.""" - coordinator_key: CoordinatorKeyType - value_fn: Callable[[Any], StateType] + value_fn: Callable[[dict[str, Any]], StateType] @dataclass @@ -39,9 +34,8 @@ class BaseEntityDescription(SensorEntityDescription): """Describes GitHub sensor entity default overrides.""" icon: str = "mdi:github" - entity_registry_enabled_default: bool = False - attr_fn: Callable[[Any], Mapping[str, Any] | None] = lambda data: None - avabl_fn: Callable[[Any], bool] = lambda data: True + attr_fn: Callable[[dict[str, Any]], Mapping[str, Any] | None] = lambda data: None + avabl_fn: Callable[[dict[str, Any]], bool] = lambda data: True @dataclass @@ -50,6 +44,14 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( + GitHubSensorEntityDescription( + key="discussions_count", + name="Discussions", + native_unit_of_measurement="Discussions", + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data["discussion"]["total"], + ), GitHubSensorEntityDescription( key="stargazers_count", name="Stars", @@ -57,8 +59,7 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription native_unit_of_measurement="Stars", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.stargazers_count, - coordinator_key="information", + value_fn=lambda data: data["stargazers_count"], ), GitHubSensorEntityDescription( key="subscribers_count", @@ -67,9 +68,7 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription native_unit_of_measurement="Watchers", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - # The API returns a watcher_count, but subscribers_count is more accurate - value_fn=lambda data: data.subscribers_count, - coordinator_key="information", + value_fn=lambda data: data["watchers"]["total"], ), GitHubSensorEntityDescription( key="forks_count", @@ -78,8 +77,7 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription native_unit_of_measurement="Forks", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.forks_count, - coordinator_key="information", + value_fn=lambda data: data["forks_count"], ), GitHubSensorEntityDescription( key="issues_count", @@ -87,8 +85,7 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription native_unit_of_measurement="Issues", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.issues_count, - coordinator_key="issue", + value_fn=lambda data: data["issue"]["total"], ), GitHubSensorEntityDescription( key="pulls_count", @@ -96,50 +93,64 @@ class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescription native_unit_of_measurement="Pull Requests", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda data: data.pulls_count, - coordinator_key="issue", + value_fn=lambda data: data["pull_request"]["total"], ), GitHubSensorEntityDescription( - coordinator_key="commit", key="latest_commit", name="Latest Commit", - value_fn=lambda data: data.commit.message.splitlines()[0][:255], + value_fn=lambda data: data["default_branch_ref"]["commit"]["message"][:255], attr_fn=lambda data: { - "sha": data.sha, - "url": data.html_url, + "sha": data["default_branch_ref"]["commit"]["sha"], + "url": data["default_branch_ref"]["commit"]["url"], + }, + ), + GitHubSensorEntityDescription( + key="latest_discussion", + name="Latest Discussion", + avabl_fn=lambda data: data["discussion"]["discussions"], + value_fn=lambda data: data["discussion"]["discussions"][0]["title"][:255], + attr_fn=lambda data: { + "url": data["discussion"]["discussions"][0]["url"], + "number": data["discussion"]["discussions"][0]["number"], }, ), GitHubSensorEntityDescription( - coordinator_key="release", key="latest_release", name="Latest Release", - entity_registry_enabled_default=True, - value_fn=lambda data: data.name[:255], + avabl_fn=lambda data: data["release"] is not None, + value_fn=lambda data: data["release"]["name"][:255], attr_fn=lambda data: { - "url": data.html_url, - "tag": data.tag_name, + "url": data["release"]["url"], + "tag": data["release"]["tag"], }, ), GitHubSensorEntityDescription( - coordinator_key="issue", key="latest_issue", name="Latest Issue", - value_fn=lambda data: data.issue_last.title[:255], - avabl_fn=lambda data: data.issue_last is not None, + avabl_fn=lambda data: data["issue"]["issues"], + value_fn=lambda data: data["issue"]["issues"][0]["title"][:255], attr_fn=lambda data: { - "url": data.issue_last.html_url, - "number": data.issue_last.number, + "url": data["issue"]["issues"][0]["url"], + "number": data["issue"]["issues"][0]["number"], }, ), GitHubSensorEntityDescription( - coordinator_key="issue", key="latest_pull_request", name="Latest Pull Request", - value_fn=lambda data: data.pull_last.title[:255], - avabl_fn=lambda data: data.pull_last is not None, + avabl_fn=lambda data: data["pull_request"]["pull_requests"], + value_fn=lambda data: data["pull_request"]["pull_requests"][0]["title"][:255], attr_fn=lambda data: { - "url": data.pull_last.html_url, - "number": data.pull_last.number, + "url": data["pull_request"]["pull_requests"][0]["url"], + "number": data["pull_request"]["pull_requests"][0]["number"], + }, + ), + GitHubSensorEntityDescription( + key="latest_tag", + name="Latest Tag", + avabl_fn=lambda data: data["refs"]["tags"], + value_fn=lambda data: data["refs"]["tags"][0]["name"][:255], + attr_fn=lambda data: { + "url": data["refs"]["tags"][0]["target"]["url"], }, ), ) @@ -151,43 +162,41 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up GitHub sensor based on a config entry.""" - repositories: dict[str, DataUpdateCoordinators] = hass.data[DOMAIN] + repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] async_add_entities( ( - GitHubSensorEntity(coordinators, description) + GitHubSensorEntity(coordinator, description) for description in SENSOR_DESCRIPTIONS - for coordinators in repositories.values() + for coordinator in repositories.values() ), - update_before_add=True, ) -class GitHubSensorEntity(CoordinatorEntity, SensorEntity): +class GitHubSensorEntity(CoordinatorEntity[dict[str, Any]], SensorEntity): """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" - coordinator: GitHubBaseDataUpdateCoordinator + coordinator: GitHubDataUpdateCoordinator entity_description: GitHubSensorEntityDescription def __init__( self, - coordinators: DataUpdateCoordinators, + coordinator: GitHubDataUpdateCoordinator, entity_description: GitHubSensorEntityDescription, ) -> None: """Initialize the sensor.""" - coordinator = coordinators[entity_description.coordinator_key] - _information = coordinators["information"].data - super().__init__(coordinator=coordinator) self.entity_description = entity_description - self._attr_name = f"{_information.full_name} {entity_description.name}" - self._attr_unique_id = f"{_information.id}_{entity_description.key}" + self._attr_name = ( + f"{coordinator.data.get('full_name')} {entity_description.name}" + ) + self._attr_unique_id = f"{coordinator.data.get('id')}_{entity_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.repository)}, - name=_information.full_name, + name=coordinator.data.get("full_name"), manufacturer="GitHub", configuration_url=f"https://github.com/{coordinator.repository}", entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/github/translations/bg.json b/homeassistant/components/github/translations/bg.json index 80a7cc489a9b0..1a52d69dc2dca 100644 --- a/homeassistant/components/github/translations/bg.json +++ b/homeassistant/components/github/translations/bg.json @@ -2,6 +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" + }, + "step": { + "repositories": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430" + } } } } \ No newline at end of file diff --git a/homeassistant/components/github/translations/el.json b/homeassistant/components/github/translations/el.json index cc5672a63b8e8..97abf5d3d0c9f 100644 --- a/homeassistant/components/github/translations/el.json +++ b/homeassistant/components/github/translations/el.json @@ -1,6 +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", "could_not_register": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03bf GitHub" }, "progress": { diff --git a/homeassistant/components/github/translations/fr.json b/homeassistant/components/github/translations/fr.json index 8a9f2a08ba4ac..d159c86efaab9 100644 --- a/homeassistant/components/github/translations/fr.json +++ b/homeassistant/components/github/translations/fr.json @@ -6,6 +6,14 @@ }, "progress": { "wait_for_device": "1. Ouvrez {url}\n2. Collez la cl\u00e9 suivante pour autoriser l'int\u00e9gration\u00a0:\n ```\n {code}\n ```\n" + }, + "step": { + "repositories": { + "data": { + "repositories": "S\u00e9lectionnez les r\u00e9f\u00e9rentiels \u00e0 suivre." + }, + "title": "Configurer les r\u00e9f\u00e9rentiels" + } } } } \ No newline at end of file diff --git a/homeassistant/components/github/translations/id.json b/homeassistant/components/github/translations/id.json index cd133547d61d6..93c2616be9946 100644 --- a/homeassistant/components/github/translations/id.json +++ b/homeassistant/components/github/translations/id.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "could_not_register": "Tidak dapat mendaftarkan integrasi dengan GitHub" + }, + "progress": { + "wait_for_device": "1. Buka {url} \n2.Tempelkan kunci berikut untuk mengotorisasi integrasi: \n```\n{code}\n```\n" }, "step": { "repositories": { diff --git a/homeassistant/components/github/translations/nl.json b/homeassistant/components/github/translations/nl.json index 8dbc3289c81ca..f7cb2ac22e407 100644 --- a/homeassistant/components/github/translations/nl.json +++ b/homeassistant/components/github/translations/nl.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd", + "could_not_register": "Kan integratie niet met GitHub registreren" + }, + "progress": { + "wait_for_device": "1. Open {url} \n2.Plak de volgende sleutel om de integratie te autoriseren: \n```\n{code}\n```\n" }, "step": { "repositories": { diff --git a/homeassistant/components/github/translations/pl.json b/homeassistant/components/github/translations/pl.json index 79f1444ff4461..857958a849091 100644 --- a/homeassistant/components/github/translations/pl.json +++ b/homeassistant/components/github/translations/pl.json @@ -1,17 +1,18 @@ { "config": { "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", "could_not_register": "Nie mo\u017cna zarejestrowa\u0107 integracji z GitHub" }, "progress": { - "wait_for_device": "1. Otw\u00f3rz {url}\n 2. Wklej nast\u0119puj\u0105cy klucz, aby autoryzowa\u0107 integracj\u0119:\n ```\n {code}\n ```\n" + "wait_for_device": "1. Otw\u00f3rz {url}\n2. Wklej nast\u0119puj\u0105cy klucz, aby autoryzowa\u0107 integracj\u0119:\n ```\n {code}\n ```\n" }, "step": { "repositories": { "data": { "repositories": "Wybierz repozytoria do \u015bledzenia." }, - "title": "Konfiguruj repozytoria" + "title": "Konfiguracja repozytori\u00f3w" } } } diff --git a/homeassistant/components/github/translations/sk.json b/homeassistant/components/github/translations/sk.json new file mode 100644 index 0000000000000..f04d4a327f48d --- /dev/null +++ b/homeassistant/components/github/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/github/translations/zh-Hans.json b/homeassistant/components/github/translations/zh-Hans.json new file mode 100644 index 0000000000000..74f09833a817c --- /dev/null +++ b/homeassistant/components/github/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "could_not_register": "\u65e0\u6cd5\u4f7f\u96c6\u6210\u4e0e Github \u6ce8\u518c" + }, + "step": { + "repositories": { + "data": { + "repositories": "\u9009\u62e9\u60a8\u60f3\u8981\u8ddf\u8e2a\u7684\u4ed3\u5e93(Repo)" + }, + "title": "\u914d\u7f6e\u4ed3\u5e93(Repo)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 77852e6d9828d..0761962375603 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": ["python-gitlab==1.6.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["gitlab"] } diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index bbf02d1ec9eac..efd4ff3d28bbd 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": ["gitterpy==0.1.7"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["gitterpy"] } diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 22f6ef2ed1df1..6272015e73c76 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -163,7 +163,7 @@ async def refresh(event_time): ) @staticmethod - async def async_options_updated(hass, entry): + 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] diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index a25ae1b46608a..e5a8f1424c2b7 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -19,7 +19,7 @@ DATA_UPDATED = "glances_data_updated" SUPPORTED_VERSIONS = [2, 3] -if sys.maxsize > 2 ** 32: +if sys.maxsize > 2**32: CPU_ICON = "mdi:cpu-64-bit" else: CPU_ICON = "mdi:cpu-32-bit" diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 2b0b0f1a4eea2..cce95957ff612 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": ["glances_api==0.3.4"], "codeowners": ["@fabaff", "@engrbm87"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["glances_api"] } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index b28ccd86b84fe..a907dd1695ad9 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -129,14 +129,14 @@ async def async_update(self): # noqa: C901 break if self.entity_description.key == "disk_free": try: - self._state = round(disk["free"] / 1024 ** 3, 1) + self._state = round(disk["free"] / 1024**3, 1) except KeyError: self._state = round( - (disk["size"] - disk["used"]) / 1024 ** 3, + (disk["size"] - disk["used"]) / 1024**3, 1, ) elif self.entity_description.key == "disk_use": - self._state = round(disk["used"] / 1024 ** 3, 1) + self._state = round(disk["used"] / 1024**3, 1) elif self.entity_description.key == "disk_use_percent": self._state = disk["percent"] elif self.entity_description.key == "battery": @@ -170,15 +170,15 @@ async def async_update(self): # noqa: C901 elif self.entity_description.key == "memory_use_percent": self._state = value["mem"]["percent"] elif self.entity_description.key == "memory_use": - self._state = round(value["mem"]["used"] / 1024 ** 2, 1) + self._state = round(value["mem"]["used"] / 1024**2, 1) elif self.entity_description.key == "memory_free": - self._state = round(value["mem"]["free"] / 1024 ** 2, 1) + self._state = round(value["mem"]["free"] / 1024**2, 1) elif self.entity_description.key == "swap_use_percent": self._state = value["memswap"]["percent"] elif self.entity_description.key == "swap_use": - self._state = round(value["memswap"]["used"] / 1024 ** 3, 1) + self._state = round(value["memswap"]["used"] / 1024**3, 1) elif self.entity_description.key == "swap_free": - self._state = round(value["memswap"]["free"] / 1024 ** 3, 1) + self._state = round(value["memswap"]["free"] / 1024**3, 1) elif self.entity_description.key == "processor_load": # Windows systems don't provide load details try: @@ -219,7 +219,7 @@ async def async_update(self): # noqa: C901 for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: mem_use += container["memory"]["usage"] - self._state = round(mem_use / 1024 ** 2, 1) + self._state = round(mem_use / 1024**2, 1) except KeyError: self._state = STATE_UNAVAILABLE elif self.entity_description.type == "raid": diff --git a/homeassistant/components/glances/translations/el.json b/homeassistant/components/glances/translations/el.json new file mode 100644 index 0000000000000..f0f927fcc7175 --- /dev/null +++ b/homeassistant/components/glances/translations/el.json @@ -0,0 +1,36 @@ +{ + "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", + "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)" + }, + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/pt-BR.json b/homeassistant/components/glances/translations/pt-BR.json index 7f5535fd04b66..d081c897d3873 100644 --- a/homeassistant/components/glances/translations/pt-BR.json +++ b/homeassistant/components/glances/translations/pt-BR.json @@ -1,12 +1,25 @@ { "config": { + "abort": { + "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)" + }, "step": { "user": { "data": { + "host": "Nome do host", + "name": "Nome", "password": "Senha", "port": "Porta", - "username": "Usu\u00e1rio" - } + "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" } } }, @@ -15,7 +28,8 @@ "init": { "data": { "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" - } + }, + "description": "Configure op\u00e7\u00f5es para Glances" } } } diff --git a/homeassistant/components/glances/translations/sk.json b/homeassistant/components/glances/translations/sk.json new file mode 100644 index 0000000000000..39d2e182c40be --- /dev/null +++ b/homeassistant/components/glances/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gntp/__init__.py b/homeassistant/components/gntp/__init__.py deleted file mode 100644 index c2814f86f06d8..0000000000000 --- a/homeassistant/components/gntp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gntp component.""" diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json deleted file mode 100644 index ebef78f9e7fd1..0000000000000 --- a/homeassistant/components/gntp/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "gntp", - "name": "Growl (GnGNTP)", - "documentation": "https://www.home-assistant.io/integrations/gntp", - "requirements": ["gntp==1.0.3"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py deleted file mode 100644 index b3291e256174c..0000000000000 --- a/homeassistant/components/gntp/notify.py +++ /dev/null @@ -1,96 +0,0 @@ -"""GNTP (aka Growl) notification service.""" -import logging -import os - -import gntp.errors -import gntp.notifier -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.const import CONF_PASSWORD, CONF_PORT -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_APP_NAME = "app_name" -CONF_APP_ICON = "app_icon" -CONF_HOSTNAME = "hostname" - -DEFAULT_APP_NAME = "HomeAssistant" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 23053 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_APP_NAME, default=DEFAULT_APP_NAME): cv.string, - vol.Optional(CONF_APP_ICON): vol.Url, - vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the GNTP notification service.""" - _LOGGER.warning( - "The GNTP (Growl) integration has been deprecated and is going to be " - "removed in Home Assistant Core 2021.6. The Growl project has retired" - ) - - logging.getLogger("gntp").setLevel(logging.ERROR) - - if config.get(CONF_APP_ICON) is None: - icon_file = os.path.join( - os.path.dirname(__file__), - "..", - "frontend", - "www_static", - "icons", - "favicon-192x192.png", - ) - with open(icon_file, "rb") as file: - app_icon = file.read() - else: - app_icon = config.get(CONF_APP_ICON) - - return GNTPNotificationService( - config.get(CONF_APP_NAME), - app_icon, - config.get(CONF_HOSTNAME), - config.get(CONF_PASSWORD), - config.get(CONF_PORT), - ) - - -class GNTPNotificationService(BaseNotificationService): - """Implement the notification service for GNTP.""" - - def __init__(self, app_name, app_icon, hostname, password, port): - """Initialize the service.""" - self.gntp = gntp.notifier.GrowlNotifier( - applicationName=app_name, - notifications=["Notification"], - applicationIcon=app_icon, - hostname=hostname, - password=password, - port=port, - ) - try: - self.gntp.register() - except gntp.errors.NetworkError: - _LOGGER.error("Unable to register with the GNTP host") - return - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - self.gntp.notify( - noteType="Notification", - title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - description=message, - ) diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index aed773350f6a9..68e12887daf72 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": ["pysher==1.0.7"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pysher"] } diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index f46401d2a6be4..5cb00c11191af 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -5,9 +5,11 @@ "documentation": "https://www.home-assistant.io/integrations/goalzero", "requirements": ["goalzero==0.2.1"], "dhcp": [ + {"registered_devices": true}, {"hostname": "yeti*"} ], "codeowners": ["@tkdrob"], "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["goalzero"] } diff --git a/homeassistant/components/goalzero/translations/cs.json b/homeassistant/components/goalzero/translations/cs.json index 4d39a29a7c379..2f2735f3c45ed 100644 --- a/homeassistant/components/goalzero/translations/cs.json +++ b/homeassistant/components/goalzero/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 61936c6ff5608..31d089f53ec55 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", + "invalid_host": "\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", + "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", @@ -9,6 +11,10 @@ "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "confirm_discovery": { + "description": "\u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 ip. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", + "title": "Goal Zero Yeti" + }, "user": { "data": { "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", diff --git a/homeassistant/components/goalzero/translations/pt-BR.json b/homeassistant/components/goalzero/translations/pt-BR.json new file mode 100644 index 0000000000000..7ecea696702b6 --- /dev/null +++ b/homeassistant/components/goalzero/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "confirm_discovery": { + "description": "A reserva de DHCP em seu roteador \u00e9 recomendada. Se n\u00e3o estiver configurado, o dispositivo pode ficar indispon\u00edvel at\u00e9 que o Home Assistant detecte o novo endere\u00e7o IP. Consulte o manual do usu\u00e1rio do seu roteador.", + "title": "Gol Zero Yeti" + }, + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Primeiro, voc\u00ea precisa baixar o aplicativo Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n Siga as instru\u00e7\u00f5es para conectar seu Yeti \u00e0 sua rede Wi-fi. A reserva de DHCP em seu roteador \u00e9 recomendada. Se n\u00e3o estiver configurado, o dispositivo pode ficar indispon\u00edvel at\u00e9 que o Home Assistant detecte o novo endere\u00e7o IP. Consulte o manual do usu\u00e1rio do seu roteador.", + "title": "Gol Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/sk.json b/homeassistant/components/goalzero/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/goalzero/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 90d50bdda4335..b438174c25609 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -13,5 +13,6 @@ "hostname": "ismartgate*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ismartgate"] } diff --git a/homeassistant/components/gogogate2/translations/el.json b/homeassistant/components/gogogate2/translations/el.json index dc8a810dbe3f3..373d36bb397a0 100644 --- a/homeassistant/components/gogogate2/translations/el.json +++ b/homeassistant/components/gogogate2/translations/el.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "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", + "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" + }, "flow_title": "{device} ({ip_address})", "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c0\u03b1\u03c1\u03b1\u03af\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Gogogate2 \u03ae ismartgate" } diff --git a/homeassistant/components/gogogate2/translations/pt-BR.json b/homeassistant/components/gogogate2/translations/pt-BR.json index 79dc7af8131fd..99b63826b307e 100644 --- a/homeassistant/components/gogogate2/translations/pt-BR.json +++ b/homeassistant/components/gogogate2/translations/pt-BR.json @@ -1,14 +1,22 @@ { "config": { + "abort": { + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { "ip_address": "Endere\u00e7o IP", "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "description": "Forne\u00e7a as informa\u00e7\u00f5es necess\u00e1rias abaixo.", - "title": "Configurar GogoGate2" + "title": "Configurar Gogogate2 ou ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/sk.json b/homeassistant/components/gogogate2/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 1116451e15f5e..102cd0ac2eb79 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -8,5 +8,6 @@ ], "requirements": ["goodwe==0.2.15"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["goodwe"] } \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/el.json b/homeassistant/components/goodwe/translations/el.json index 7afcd76e1b381..431644cce7b41 100644 --- a/homeassistant/components/goodwe/translations/el.json +++ b/homeassistant/components/goodwe/translations/el.json @@ -1,7 +1,17 @@ { "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" + }, + "error": { + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1", "title": "\u039c\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 GoodWe" } diff --git a/homeassistant/components/goodwe/translations/fr.json b/homeassistant/components/goodwe/translations/fr.json index 8e5504b3ee496..544d6cfda6896 100644 --- a/homeassistant/components/goodwe/translations/fr.json +++ b/homeassistant/components/goodwe/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" }, "error": { "connection_error": "Impossible de se connecter" @@ -11,7 +12,8 @@ "data": { "host": "Adresse IP" }, - "description": "Connecter \u00e0 l'onduleur" + "description": "Connecter \u00e0 l'onduleur", + "title": "Onduleur GoodWe" } } } diff --git a/homeassistant/components/goodwe/translations/nl.json b/homeassistant/components/goodwe/translations/nl.json index d648ff4e8e8fd..8986006fdeb25 100644 --- a/homeassistant/components/goodwe/translations/nl.json +++ b/homeassistant/components/goodwe/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang" }, "error": { diff --git a/homeassistant/components/goodwe/translations/pt-BR.json b/homeassistant/components/goodwe/translations/pt-BR.json new file mode 100644 index 0000000000000..14b1971e62580 --- /dev/null +++ b/homeassistant/components/goodwe/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento" + }, + "error": { + "connection_error": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + }, + "description": "Conecte ao inversor", + "title": "Inversor GoodWe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/sk.json b/homeassistant/components/goodwe/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/goodwe/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 0dfacb13e740f..9f85d41774bea 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,8 +1,10 @@ """Support for Google - Calendar Event Devices.""" +from collections.abc import Mapping from datetime import datetime, timedelta, timezone from enum import Enum import logging import os +from typing import Any from googleapiclient import discovery as google_discovery import httplib2 @@ -158,7 +160,9 @@ def scope(self) -> str: ) -def do_authentication(hass, hass_config, config): +def do_authentication( + hass: HomeAssistant, hass_config: ConfigType, config: ConfigType +) -> bool: """Notify user of actions and authenticate. Notify user of user_code and verification_url then poll @@ -192,8 +196,9 @@ def do_authentication(hass, hass_config, config): notification_id=NOTIFICATION_ID, ) - def step2_exchange(now): + def step2_exchange(now: datetime) -> None: """Keep trying to validate the user_code until it expires.""" + _LOGGER.debug("Attempting to validate user code") # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime # object without tzinfo. For the comparison below to work, it needs one. @@ -208,6 +213,7 @@ def step2_exchange(now): notification_id=NOTIFICATION_ID, ) listener() + return try: credentials = oauth.step2_exchange(device_flow_info=dev_flow) @@ -247,9 +253,11 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: token_file = hass.config.path(TOKEN_FILE) if not os.path.isfile(token_file): + _LOGGER.debug("Token file does not exist, authenticating for first time") do_authentication(hass, config, conf) else: - if not check_correct_scopes(token_file, conf): + if not check_correct_scopes(hass, token_file, conf): + _LOGGER.debug("Existing scopes are not sufficient, re-authenticating") do_authentication(hass, config, conf) else: do_setup(hass, config, conf) @@ -257,22 +265,41 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def check_correct_scopes(token_file, config): +def check_correct_scopes( + hass: HomeAssistant, token_file: str, config: ConfigType +) -> bool: """Check for the correct scopes in file.""" - with open(token_file, encoding="utf8") as tokenfile: - contents = tokenfile.read() - - # Check for quoted scope as our scopes can be subsets of other scopes - target_scope = f'"{config.get(CONF_CALENDAR_ACCESS).scope}"' - if target_scope not in contents: - _LOGGER.warning("Please re-authenticate with Google") - return False - return True + creds = Storage(token_file).get() + if not creds or not creds.scopes: + return False + target_scope = config[CONF_CALENDAR_ACCESS].scope + return target_scope in creds.scopes + + +class GoogleCalendarService: + """Calendar service interface to Google.""" + + def __init__(self, token_file: str) -> None: + """Init the Google Calendar service.""" + self.token_file = token_file + + def get(self) -> google_discovery.Resource: + """Get the calendar service from the storage file token.""" + credentials = Storage(self.token_file).get() + http = credentials.authorize(httplib2.Http()) + service = google_discovery.build( + "calendar", "v3", http=http, cache_discovery=False + ) + return service def setup_services( - hass, hass_config, config, track_new_found_calendars, calendar_service -): + hass: HomeAssistant, + hass_config: ConfigType, + config: ConfigType, + track_new_found_calendars: bool, + calendar_service: GoogleCalendarService, +) -> None: """Set up the service listeners.""" def _found_calendar(call: ServiceCall) -> None: @@ -359,11 +386,11 @@ def _add_event(call: ServiceCall) -> None: hass.services.register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA ) - return True -def do_setup(hass, hass_config, config): +def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) -> None: """Run the setup after we have everything configured.""" + _LOGGER.debug("Setting up integration") # Load calendars the user has configured hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) @@ -371,6 +398,7 @@ def do_setup(hass, hass_config, config): track_new_found_calendars = convert( config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW ) + assert track_new_found_calendars is not None setup_services( hass, hass_config, config, track_new_found_calendars, calendar_service ) @@ -380,29 +408,13 @@ def do_setup(hass, hass_config, config): # Look for any new calendars hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) - return True - - -class GoogleCalendarService: - """Calendar service interface to Google.""" - - def __init__(self, token_file): - """Init the Google Calendar service.""" - self.token_file = token_file - - def get(self): - """Get the calendar service from the storage file token.""" - credentials = Storage(self.token_file).get() - http = credentials.authorize(httplib2.Http()) - service = google_discovery.build( - "calendar", "v3", http=http, cache_discovery=False - ) - return service -def get_calendar_info(hass, calendar): +def get_calendar_info( + hass: HomeAssistant, calendar: Mapping[str, Any] +) -> dict[str, Any]: """Convert data from Google into DEVICE_SCHEMA.""" - calendar_info = DEVICE_SCHEMA( + calendar_info: dict[str, Any] = DEVICE_SCHEMA( { CONF_CAL_ID: calendar["id"], CONF_ENTITIES: [ @@ -419,7 +431,7 @@ def get_calendar_info(hass, calendar): return calendar_info -def load_config(path): +def load_config(path: str) -> dict[str, Any]: """Load the google_calendar_devices.yaml.""" calendars = {} try: @@ -438,7 +450,7 @@ def load_config(path): return calendars -def update_config(path, calendar): +def update_config(path: str, calendar: dict[str, Any]) -> None: """Write the google_calendar_devices.yaml.""" with open(path, "a", encoding="utf8") as out: out.write("\n") diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 6db9c810ad99b..90b5bbb3d8937 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -2,9 +2,11 @@ from __future__ import annotations import copy -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any +from googleapiclient import discovery as google_discovery from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( @@ -72,40 +74,48 @@ def setup_platform( class GoogleCalendarEventDevice(CalendarEventDevice): """A calendar event device.""" - def __init__(self, calendar_service, calendar, data, entity_id): + def __init__( + self, + calendar_service: GoogleCalendarService, + calendar_id: str, + data: dict[str, Any], + entity_id: str, + ) -> None: """Create the Calendar event device.""" self.data = GoogleCalendarData( calendar_service, - calendar, + calendar_id, data.get(CONF_SEARCH), - data.get(CONF_IGNORE_AVAILABILITY), + data.get(CONF_IGNORE_AVAILABILITY, False), ) - self._event = None - self._name = data[CONF_NAME] + self._event: dict[str, Any] | None = None + self._name: str = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_reached = False self.entity_id = entity_id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" return {"offset_reached": self._offset_reached} @property - def event(self): + def event(self) -> dict[str, Any] | None: """Return the next upcoming event.""" return self._event @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[dict[str, Any]]: """Get all events in a specific time frame.""" return await self.data.async_get_events(hass, start_date, end_date) - def update(self): + def update(self) -> None: """Update event data.""" self.data.update() event = copy.deepcopy(self.data.event) @@ -120,15 +130,23 @@ def update(self): class GoogleCalendarData: """Class to utilize calendar service object to get next event.""" - def __init__(self, calendar_service, calendar_id, search, ignore_availability): + def __init__( + self, + calendar_service: GoogleCalendarService, + calendar_id: str, + search: str | None, + ignore_availability: bool, + ) -> None: """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id self.search = search self.ignore_availability = ignore_availability - self.event = None + self.event: dict[str, Any] | None = None - def _prepare_query(self): + def _prepare_query( + self, + ) -> tuple[google_discovery.Resource | None, dict[str, Any] | None]: try: service = self.calendar_service.get() except ServerNotFoundError as err: @@ -143,17 +161,19 @@ def _prepare_query(self): return service, params - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[dict[str, Any]]: """Get all events in a specific time frame.""" service, params = await hass.async_add_executor_job(self._prepare_query) - if service is None: + if service is None or params is None: return [] params["timeMin"] = start_date.isoformat("T") params["timeMax"] = end_date.isoformat("T") - event_list = [] + event_list: list[dict[str, Any]] = [] events = await hass.async_add_executor_job(service.events) - page_token = None + page_token: str | None = None while True: page_token = await self.async_get_events_page( hass, events, params, page_token, event_list @@ -162,7 +182,14 @@ async def async_get_events(self, hass, start_date, end_date): break return event_list - async def async_get_events_page(self, hass, events, params, page_token, event_list): + async def async_get_events_page( + self, + hass: HomeAssistant, + events: google_discovery.Resource, + params: dict[str, Any], + page_token: str | None, + event_list: list[dict[str, Any]], + ) -> str | None: """Get a page of events in a specific time frame.""" params["pageToken"] = page_token result = await hass.async_add_executor_job(events.list(**params).execute) @@ -177,10 +204,10 @@ async def async_get_events_page(self, hass, events, params, page_token, event_li return result.get("nextPageToken") @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data.""" service, params = self._prepare_query() - if service is None: + if service is None or params is None: return params["timeMin"] = dt.now().isoformat("T") diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 00d76a9c1d050..1695c7d0d843d 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -8,5 +8,6 @@ "oauth2client==4.1.3" ], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["googleapiclient"] } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index b220119691773..a348299e28221 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from asyncio import gather from collections.abc import Mapping +from datetime import datetime, timedelta from http import HTTPStatus import logging import pprint @@ -26,6 +27,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store +from homeassistant.util.dt import utcnow from . import trait from .const import ( @@ -104,6 +106,7 @@ def __init__(self, hass): self._store = None self._google_sync_unsub = {} self._local_sdk_active = False + self._local_last_active: datetime | None = None async def async_initialize(self): """Perform async initialization of config.""" @@ -149,6 +152,15 @@ def should_report_state(self): """Return if states should be proactively reported.""" return False + @property + def is_local_connected(self) -> bool: + """Return if local is connected.""" + return ( + self._local_last_active is not None + # We get a reachable devices intent every minute. + and self._local_last_active > utcnow() - timedelta(seconds=70) + ) + def get_local_agent_user_id(self, webhook_id): """Return the user ID to be used for actions received via the local SDK. @@ -336,6 +348,7 @@ async def _handle_local_webhook(self, hass, webhook_id, request): # pylint: disable=import-outside-toplevel from . import smart_home + self._local_last_active = utcnow() payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 31fd02544ff19..5f38194e3e3b8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -19,7 +19,7 @@ EXECUTE_LIMIT = 2 # Wait 2 seconds for execute to finish -HANDLERS = Registry() +HANDLERS = Registry() # type: ignore[var-annotated] _LOGGER = logging.getLogger(__name__) @@ -281,7 +281,7 @@ async def async_devices_identify(hass, data: RequestData, payload): async def async_devices_reachable(hass, data: RequestData, payload): """Handle action.devices.REACHABLE_DEVICES request. - https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + https://developers.google.com/assistant/smarthome/develop/local#implement_the_reachable_devices_handler_hub_integrations_only """ google_ids = {dev["id"] for dev in (data.devices or [])} diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5b3a2db0b805f..20191c616685f 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -774,14 +774,10 @@ async def _execute_cover(self, command, data, params, challenge): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params["start"] is False: - if ( - self.state.state - in ( - cover.STATE_CLOSING, - cover.STATE_OPENING, - ) - or self.state.attributes.get(ATTR_ASSUMED_STATE) - ): + if self.state.state in ( + cover.STATE_CLOSING, + cover.STATE_OPENING, + ) or self.state.attributes.get(ATTR_ASSUMED_STATE): await self.hass.services.async_call( self.state.domain, cover.SERVICE_STOP_COVER, diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index f0f403912a630..e8c3af2339816 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": ["locationsharinglib==4.1.5"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["locationsharinglib"] } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index b566f3447f470..70f5e129950d1 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": ["gTTS==2.2.3"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["gtts"] } diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py index 6b9b77242ba29..893c0e48bd040 100644 --- a/homeassistant/components/google_travel_time/const.py +++ b/homeassistant/components/google_travel_time/const.py @@ -26,8 +26,6 @@ DEFAULT_NAME = "Google Travel Time" -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - ALL_LANGUAGES = [ "ar", "bg", diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index 8800b4ef4b8a4..4b2693374f67c 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -3,7 +3,8 @@ "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": ["googlemaps==2.5.1"], - "codeowners": [], + "codeowners": ["@eifinger"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["googlemaps"] } diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6960361d9a1c8..3ee2a18455c17 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -35,7 +35,6 @@ CONF_UNITS, DEFAULT_NAME, DOMAIN, - TRACKABLE_DOMAINS, ) _LOGGER = logging.getLogger(__name__) @@ -109,17 +108,10 @@ def __init__(self, config_entry, name, api_key, origin, destination, client): self._api_key = api_key self._unique_id = config_entry.entry_id self._client = client - - # Check if location is a trackable entity - if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._origin_entity_id = origin - else: - self._origin = origin - - if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._destination_entity_id = destination - else: - self._destination = destination + self._origin = origin + self._destination = destination + self._resolved_origin = None + self._resolved_destination = None async def async_added_to_hass(self) -> None: """Handle when entity is added.""" @@ -179,8 +171,8 @@ def extra_state_attributes(self): res["duration"] = _data["duration"]["text"] if "distance" in _data: res["distance"] = _data["distance"]["text"] - res["origin"] = self._origin - res["destination"] = self._destination + res["origin"] = self._resolved_origin + res["destination"] = self._resolved_destination res[ATTR_ATTRIBUTION] = ATTRIBUTION return res @@ -211,14 +203,18 @@ def update(self): elif atime is not None: options_copy[CONF_ARRIVAL_TIME] = atime - # Convert device_trackers to google friendly location - if hasattr(self, "_origin_entity_id"): - self._origin = find_coordinates(self.hass, self._origin_entity_id) - - if hasattr(self, "_destination_entity_id"): - self._destination = find_coordinates(self.hass, self._destination_entity_id) + self._resolved_origin = find_coordinates(self.hass, self._origin) + self._resolved_destination = find_coordinates(self.hass, self._destination) - if self._destination is not None and self._origin is not None: + _LOGGER.debug( + "Getting update for origin: %s destination: %s", + self._resolved_origin, + self._resolved_destination, + ) + if self._resolved_destination is not None and self._resolved_origin is not None: self._matrix = distance_matrix( - self._client, self._origin, self._destination, **options_copy + self._client, + self._resolved_origin, + self._resolved_destination, + **options_copy, ) diff --git a/homeassistant/components/google_travel_time/translations/el.json b/homeassistant/components/google_travel_time/translations/el.json index 5d5d0dcf9a579..8180ef61c03cd 100644 --- a/homeassistant/components/google_travel_time/translations/el.json +++ b/homeassistant/components/google_travel_time/translations/el.json @@ -1,9 +1,17 @@ { "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "user": { "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "destination": "\u03a0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "origin": "\u03a0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7" }, "description": "\u038c\u03c4\u03b1\u03bd \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03bc\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf pipe (\"|\"), \u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2, \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03bf\u03cd \u03c0\u03bb\u03ac\u03c4\u03bf\u03c5\u03c2/\u03bc\u03ae\u03ba\u03bf\u03c5\u03c2 \u03ae \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c4\u03cc\u03c0\u03bf\u03c5 Google. \u038c\u03c4\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03cc\u03c0\u03bf\u03c5 Google, \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c9\u03c2 \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03c4\u03bf `place_id:`." diff --git a/homeassistant/components/google_travel_time/translations/pt-BR.json b/homeassistant/components/google_travel_time/translations/pt-BR.json new file mode 100644 index 0000000000000..b08969910975b --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pt-BR.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "destination": "Destino", + "name": "Nome", + "origin": "Origem" + }, + "description": "Ao especificar a origem e o destino, voc\u00ea pode fornecer um ou mais locais separados pelo caractere de barra vertical, na forma de um endere\u00e7o, coordenadas de latitude/longitude ou um ID de local do Google. Ao especificar o local usando um ID de local do Google, o ID deve ser prefixado com `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evite", + "language": "Idioma", + "mode": "Modo de viagem", + "time": "Tempo", + "time_type": "Tipo de tempo", + "transit_mode": "Modo de tr\u00e2nsito", + "transit_routing_preference": "Prefer\u00eancia de rota de tr\u00e2nsito", + "units": "Unidades" + }, + "description": "Opcionalmente, voc\u00ea pode especificar um hor\u00e1rio de partida ou um hor\u00e1rio de chegada. Se especificar um hor\u00e1rio de partida, voc\u00ea pode inserir `now`, um timestamp Unix ou uma string de 24 horas como `08:00:00`. Se especificar uma hora de chegada, voc\u00ea pode usar um timestamp Unix ou uma string de 24 horas como `08:00:00`now`" + } + } + }, + "title": "Tempo de viagem do Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/sk.json b/homeassistant/components/google_travel_time/translations/sk.json new file mode 100644 index 0000000000000..52d93a1a18e49 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index 9053bb7ddfcdc..b69ec09bbe7d8 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": ["gps3==0.33.3"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["gps3"] } diff --git a/homeassistant/components/gpslogger/translations/bg.json b/homeassistant/components/gpslogger/translations/bg.json index 8e1049d859e12..e396e08cfafad 100644 --- a/homeassistant/components/gpslogger/translations/bg.json +++ b/homeassistant/components/gpslogger/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/gpslogger/translations/ca.json b/homeassistant/components/gpslogger/translations/ca.json index 49878948880b2..e095a8bdb1429 100644 --- a/homeassistant/components/gpslogger/translations/ca.json +++ b/homeassistant/components/gpslogger/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/gpslogger/translations/de.json b/homeassistant/components/gpslogger/translations/de.json index 7215f0c458f62..a6ac2e228e231 100644 --- a/homeassistant/components/gpslogger/translations/de.json +++ b/homeassistant/components/gpslogger/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/gpslogger/translations/el.json b/homeassistant/components/gpslogger/translations/el.json index aecb2ee553fa4..74e1d5075aef0 100644 --- a/homeassistant/components/gpslogger/translations/el.json +++ b/homeassistant/components/gpslogger/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf GPSLogger.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03b2\u03ad\u03b2\u03b1\u03b9\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf GPSLogger Webhook;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 GPSLogger Webhook" + } } } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/en.json b/homeassistant/components/gpslogger/translations/en.json index 6d280a4c0383d..24949d0933a0f 100644 --- a/homeassistant/components/gpslogger/translations/en.json +++ b/homeassistant/components/gpslogger/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/gpslogger/translations/et.json b/homeassistant/components/gpslogger/translations/et.json index a84a69a3d0ded..641b4ba9f4aed 100644 --- a/homeassistant/components/gpslogger/translations/et.json +++ b/homeassistant/components/gpslogger/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/gpslogger/translations/fr.json b/homeassistant/components/gpslogger/translations/fr.json index 05f5985a8dfba..a69191d05c648 100644 --- a/homeassistant/components/gpslogger/translations/fr.json +++ b/homeassistant/components/gpslogger/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/gpslogger/translations/he.json b/homeassistant/components/gpslogger/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/gpslogger/translations/he.json +++ b/homeassistant/components/gpslogger/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index d458e959d0a2b..96677b262eb7a 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/gpslogger/translations/id.json b/homeassistant/components/gpslogger/translations/id.json index 3be2d91f1f364..b4e012bc5f6bf 100644 --- a/homeassistant/components/gpslogger/translations/id.json +++ b/homeassistant/components/gpslogger/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/gpslogger/translations/it.json b/homeassistant/components/gpslogger/translations/it.json index 7db05dfb8ee26..235a66c8993fd 100644 --- a/homeassistant/components/gpslogger/translations/it.json +++ b/homeassistant/components/gpslogger/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index c04d58020d60a..4674c074763db 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/gpslogger/translations/nb.json b/homeassistant/components/gpslogger/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/nl.json b/homeassistant/components/gpslogger/translations/nl.json index d90b648760db9..26bfba4eaea6a 100644 --- a/homeassistant/components/gpslogger/translations/nl.json +++ b/homeassistant/components/gpslogger/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/gpslogger/translations/no.json b/homeassistant/components/gpslogger/translations/no.json index 655cfa3841862..a46042abe87dc 100644 --- a/homeassistant/components/gpslogger/translations/no.json +++ b/homeassistant/components/gpslogger/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/gpslogger/translations/pl.json b/homeassistant/components/gpslogger/translations/pl.json index f868f770c5d2c..beb5447f89c21 100644 --- a/homeassistant/components/gpslogger/translations/pl.json +++ b/homeassistant/components/gpslogger/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/gpslogger/translations/pt-BR.json b/homeassistant/components/gpslogger/translations/pt-BR.json index fe07d42744ea2..a3204d7e5542c 100644 --- a/homeassistant/components/gpslogger/translations/pt-BR.json +++ b/homeassistant/components/gpslogger/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso webhook no GPSLogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." }, diff --git a/homeassistant/components/gpslogger/translations/ru.json b/homeassistant/components/gpslogger/translations/ru.json index 999949d5dd077..a7c9fc032f1b9 100644 --- a/homeassistant/components/gpslogger/translations/ru.json +++ b/homeassistant/components/gpslogger/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json index ef10b98c5df0b..dc14b0d4011b4 100644 --- a/homeassistant/components/gpslogger/translations/tr.json +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/gpslogger/translations/uk.json b/homeassistant/components/gpslogger/translations/uk.json index 5b0b6305cdb63..25cae99e3b024 100644 --- a/homeassistant/components/gpslogger/translations/uk.json +++ b/homeassistant/components/gpslogger/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/gpslogger/translations/zh-Hans.json b/homeassistant/components/gpslogger/translations/zh-Hans.json index 343acd4b5d919..06c91921d408f 100644 --- a/homeassistant/components/gpslogger/translations/zh-Hans.json +++ b/homeassistant/components/gpslogger/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e GPSLogger \u7684 Webhook \u529f\u80fd\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, diff --git a/homeassistant/components/gpslogger/translations/zh-Hant.json b/homeassistant/components/gpslogger/translations/zh-Hant.json index 9c5448266e66a..7d77525cc8a94 100644 --- a/homeassistant/components/gpslogger/translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index bf6fc7cc3343a..d317fe6d8732c 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Gree.""" from greeclimate.discovery import Discovery +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DISCOVERY_TIMEOUT, DOMAIN -async def _async_has_devices(hass) -> bool: +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" gree_discovery = Discovery(DISCOVERY_TIMEOUT) devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index a828789daea1b..c3b4f1f028a6e 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "requirements": ["greeclimate==1.0.2"], "codeowners": ["@cmroche"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["greeclimate"] } diff --git a/homeassistant/components/gree/translations/el.json b/homeassistant/components/gree/translations/el.json new file mode 100644 index 0000000000000..a13912159002b --- /dev/null +++ b/homeassistant/components/gree/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/pt-BR.json b/homeassistant/components/gree/translations/pt-BR.json new file mode 100644 index 0000000000000..1778d39a7d082 --- /dev/null +++ b/homeassistant/components/gree/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/uk.json b/homeassistant/components/gree/translations/uk.json index 292861e9129db..5c2489c2a18ab 100644 --- a/homeassistant/components/gree/translations/uk.json +++ b/homeassistant/components/gree/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/gree/translations/zh-Hant.json b/homeassistant/components/gree/translations/zh-Hant.json index 90c98e491dfea..cfd20d603cba1 100644 --- a/homeassistant/components/gree/translations/zh-Hant.json +++ b/homeassistant/components/gree/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index a243d767d9931..4640d062ac788 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -3,10 +3,13 @@ "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ - "greeneye_monitor==3.0.1" + "greeneye_monitor==3.0.3" ], "codeowners": [ "@jkeljo" ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": [ + "greeneye" + ] } \ No newline at end of file diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index dee2dbe75d9da..d6b7185a7cfb5 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -53,56 +53,71 @@ async def async_setup_platform( if not discovery_info: return - entities: list[GEMSensor] = [] - for monitor_config in discovery_info[CONF_MONITORS]: - monitor_serial_number = monitor_config[CONF_SERIAL_NUMBER] - - channel_configs = monitor_config[CONF_CHANNELS] - for sensor in channel_configs: - entities.append( - CurrentSensor( - monitor_serial_number, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_NET_METERING], + monitor_configs = discovery_info[CONF_MONITORS] + + def on_new_monitor(monitor: greeneye.monitor.Monitor) -> None: + monitor_config = next( + filter( + lambda monitor_config: monitor_config[CONF_SERIAL_NUMBER] + == monitor.serial_number, + monitor_configs, + ), + None, + ) + if monitor_config: + entities: list[GEMSensor] = [] + + channel_configs = monitor_config[CONF_CHANNELS] + for sensor in channel_configs: + entities.append( + CurrentSensor( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_NET_METERING], + ) ) - ) - - pulse_counter_configs = monitor_config[CONF_PULSE_COUNTERS] - for sensor in pulse_counter_configs: - entities.append( - PulseCounter( - monitor_serial_number, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_COUNTED_QUANTITY], - sensor[CONF_TIME_UNIT], - sensor[CONF_COUNTED_QUANTITY_PER_PULSE], + + pulse_counter_configs = monitor_config[CONF_PULSE_COUNTERS] + for sensor in pulse_counter_configs: + entities.append( + PulseCounter( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_COUNTED_QUANTITY], + sensor[CONF_TIME_UNIT], + sensor[CONF_COUNTED_QUANTITY_PER_PULSE], + ) ) - ) - - temperature_sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] - for sensor in temperature_sensor_configs[CONF_SENSORS]: - entities.append( - TemperatureSensor( - monitor_serial_number, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - temperature_sensor_configs[CONF_TEMPERATURE_UNIT], + + temperature_sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] + for sensor in temperature_sensor_configs[CONF_SENSORS]: + entities.append( + TemperatureSensor( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + temperature_sensor_configs[CONF_TEMPERATURE_UNIT], + ) ) - ) - - voltage_sensor_configs = monitor_config[CONF_VOLTAGE_SENSORS] - for sensor in voltage_sensor_configs: - entities.append( - VoltageSensor( - monitor_serial_number, - sensor[CONF_NUMBER], - sensor[CONF_NAME], + + voltage_sensor_configs = monitor_config[CONF_VOLTAGE_SENSORS] + for sensor in voltage_sensor_configs: + entities.append( + VoltageSensor(monitor, sensor[CONF_NUMBER], sensor[CONF_NAME]) ) - ) - async_add_entities(entities) + async_add_entities(entities) + monitor_configs.remove(monitor_config) + + if len(monitor_configs) == 0: + monitors.remove_listener(on_new_monitor) + + monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR] + monitors.add_listener(on_new_monitor) + for monitor in monitors.monitors.values(): + on_new_monitor(monitor) UnderlyingSensorType = Union[ @@ -119,13 +134,19 @@ class GEMSensor(SensorEntity): _attr_should_poll = False def __init__( - self, monitor_serial_number: int, name: str, sensor_type: str, number: int + self, + monitor: greeneye.monitor.Monitor, + name: str, + sensor_type: str, + sensor: UnderlyingSensorType, + number: int, ) -> None: """Construct the entity.""" - self._monitor_serial_number = monitor_serial_number + self._monitor = monitor + self._monitor_serial_number = self._monitor.serial_number self._attr_name = name - self._monitor: greeneye.monitor.Monitor | None = None self._sensor_type = sensor_type + self._sensor: UnderlyingSensorType = sensor self._number = number self._attr_unique_id = ( f"{self._monitor_serial_number}-{self._sensor_type}-{self._number}" @@ -133,37 +154,12 @@ def __init__( async def async_added_to_hass(self) -> None: """Wait for and connect to the sensor.""" - monitors = self.hass.data[DATA_GREENEYE_MONITOR] - - if not self._try_connect_to_monitor(monitors): - monitors.add_listener(self._on_new_monitor) - - def _on_new_monitor(self, monitor: greeneye.monitor.Monitor) -> None: - monitors = self.hass.data[DATA_GREENEYE_MONITOR] - if self._try_connect_to_monitor(monitors): - monitors.remove_listener(self._on_new_monitor) + self._sensor.add_listener(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Remove listener from the sensor.""" if self._sensor: self._sensor.remove_listener(self.async_write_ha_state) - else: - monitors = self.hass.data[DATA_GREENEYE_MONITOR] - monitors.remove_listener(self._on_new_monitor) - - def _try_connect_to_monitor(self, monitors: greeneye.Monitors) -> bool: - self._monitor = monitors.monitors.get(self._monitor_serial_number) - if not self._sensor: - return False - - self._sensor.add_listener(self.async_write_ha_state) - self.async_write_ha_state() - - return True - - @property - def _sensor(self) -> UnderlyingSensorType | None: - raise NotImplementedError() class CurrentSensor(GEMSensor): @@ -173,30 +169,25 @@ class CurrentSensor(GEMSensor): _attr_device_class = SensorDeviceClass.POWER def __init__( - self, monitor_serial_number: int, number: int, name: str, net_metering: bool + self, + monitor: greeneye.monitor.Monitor, + number: int, + name: str, + net_metering: bool, ) -> None: """Construct the entity.""" - super().__init__(monitor_serial_number, name, "current", number) + super().__init__(monitor, name, "current", monitor.channels[number - 1], number) + self._sensor: greeneye.monitor.Channel = self._sensor self._net_metering = net_metering - @property - def _sensor(self) -> greeneye.monitor.Channel | None: - return self._monitor.channels[self._number - 1] if self._monitor else None - @property def native_value(self) -> float | None: """Return the current number of watts being used by the channel.""" - if not self._sensor: - return None - return self._sensor.watts @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return total wattseconds in the state dictionary.""" - if not self._sensor: - return None - if self._net_metering: watt_seconds = self._sensor.polarized_watt_seconds else: @@ -212,7 +203,7 @@ class PulseCounter(GEMSensor): def __init__( self, - monitor_serial_number: int, + monitor: greeneye.monitor.Monitor, number: int, name: str, counted_quantity: str, @@ -220,19 +211,18 @@ def __init__( counted_quantity_per_pulse: float, ) -> None: """Construct the entity.""" - super().__init__(monitor_serial_number, name, "pulse", number) + super().__init__( + monitor, name, "pulse", monitor.pulse_counters[number - 1], number + ) + self._sensor: greeneye.monitor.PulseCounter = self._sensor self._counted_quantity_per_pulse = counted_quantity_per_pulse self._time_unit = time_unit self._attr_native_unit_of_measurement = f"{counted_quantity}/{self._time_unit}" - @property - def _sensor(self) -> greeneye.monitor.PulseCounter | None: - return self._monitor.pulse_counters[self._number - 1] if self._monitor else None - @property def native_value(self) -> float | None: """Return the current rate of change for the given pulse counter.""" - if not self._sensor or self._sensor.pulses_per_second is None: + if self._sensor.pulses_per_second is None: return None result = ( @@ -258,11 +248,8 @@ def _seconds_per_time_unit(self) -> int: ) @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, Any]: """Return total pulses in the data dictionary.""" - if not self._sensor: - return None - return {DATA_PULSES: self._sensor.pulses} @@ -272,26 +259,18 @@ class TemperatureSensor(GEMSensor): _attr_device_class = SensorDeviceClass.TEMPERATURE def __init__( - self, monitor_serial_number: int, number: int, name: str, unit: str + self, monitor: greeneye.monitor.Monitor, number: int, name: str, unit: str ) -> None: """Construct the entity.""" - super().__init__(monitor_serial_number, name, "temp", number) - self._attr_native_unit_of_measurement = unit - - @property - def _sensor(self) -> greeneye.monitor.TemperatureSensor | None: - return ( - self._monitor.temperature_sensors[self._number - 1] - if self._monitor - else None + super().__init__( + monitor, name, "temp", monitor.temperature_sensors[number - 1], number ) + self._sensor: greeneye.monitor.TemperatureSensor = self._sensor + self._attr_native_unit_of_measurement = unit @property def native_value(self) -> float | None: """Return the current temperature being reported by this sensor.""" - if not self._sensor: - return None - return self._sensor.temperature @@ -301,19 +280,14 @@ class VoltageSensor(GEMSensor): _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_device_class = SensorDeviceClass.VOLTAGE - def __init__(self, monitor_serial_number: int, number: int, name: str) -> None: + def __init__( + self, monitor: greeneye.monitor.Monitor, number: int, name: str + ) -> None: """Construct the entity.""" - super().__init__(monitor_serial_number, name, "volts", number) - - @property - def _sensor(self) -> greeneye.monitor.VoltageSensor | None: - """Wire the updates to the monitor itself, since there is no voltage element in the API.""" - return self._monitor.voltage_sensor if self._monitor else None + super().__init__(monitor, name, "volts", monitor.voltage_sensor, number) + self._sensor: greeneye.monitor.VoltageSensor = self._sensor @property def native_value(self) -> float | None: """Return the current voltage being reported by this sensor.""" - if not self._sensor: - return None - return self._sensor.voltage diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index 3d9aca1a0f90d..503719c425be8 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": ["greenwavereality==0.5.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["greenwavereality"] } diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 9c0301d97e676..de0c3d393cafe 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -17,6 +17,7 @@ CONF_UNIQUE_ID, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -80,7 +81,6 @@ def __init__( self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id self._device_class = device_class - self._state: str | None = None self.mode = any if mode: self.mode = all @@ -106,13 +106,23 @@ def async_state_changed_listener(event: Event) -> None: def async_update_group_state(self) -> None: """Query all members and determine the binary sensor group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] + + # filtered_states are members currently in the state machine filtered_states: list[str] = [x.state for x in all_states if x is not None] + + # Set group as unavailable if all members are unavailable self._attr_available = any( state != STATE_UNAVAILABLE for state in filtered_states ) - if STATE_UNAVAILABLE in filtered_states: + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + ) + if not valid_state: + # Set as unknown if any / all member is not unknown or unavailable self._attr_is_on = None else: + # Set as ON if any / all member is ON states = list(map(lambda x: x == STATE_ON, filtered_states)) state = self.mode(states) self._attr_is_on = state diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 976b8cc69f806..509c0cb40835f 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -123,7 +123,7 @@ def async_on_state_change(self, event: EventType) -> None: """Update supported features and state when a new state is received.""" self.async_set_context(event.context) self.async_update_supported_features( - event.data.get("entity_id"), event.data.get("new_state") # type: ignore + event.data.get("entity_id"), event.data.get("new_state") # type: ignore[arg-type] ) self.async_update_state() @@ -361,14 +361,14 @@ async def async_turn_off(self) -> None: async def async_volume_up(self) -> None: """Turn volume up for media player(s).""" for entity in self._features[KEY_VOLUME]: - volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore + volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr] if volume_level < 1: await self.async_set_volume_level(min(1, volume_level + 0.1)) async def async_volume_down(self) -> None: """Turn volume down for media player(s).""" for entity in self._features[KEY_VOLUME]: - volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore + volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr] if volume_level > 0: await self.async_set_volume_level(max(0, volume_level - 0.1)) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 79472359ab912..c8a71d426e724 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": ["growattServer==1.1.0"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["growattServer"] } diff --git a/homeassistant/components/growatt_server/translations/cs.json b/homeassistant/components/growatt_server/translations/cs.json index 02c83a6e9167b..31a4cdfdf0357 100644 --- a/homeassistant/components/growatt_server/translations/cs.json +++ b/homeassistant/components/growatt_server/translations/cs.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, "step": { "user": { "data": { + "name": "Jm\u00e9no", "url": "URL" } } diff --git a/homeassistant/components/growatt_server/translations/el.json b/homeassistant/components/growatt_server/translations/el.json index fe012d53d8a04..1801a2b0861c5 100644 --- a/homeassistant/components/growatt_server/translations/el.json +++ b/homeassistant/components/growatt_server/translations/el.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "no_plants": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b5\u03c1\u03b3\u03bf\u03c3\u03c4\u03ac\u03c3\u03b9\u03b1 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" + }, + "error": { + "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": { + "plant": { + "data": { + "plant_id": "\u0395\u03c1\u03b3\u03bf\u03c3\u03c4\u03ac\u03c3\u03b9\u03bf" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b5\u03c1\u03b3\u03bf\u03c3\u03c4\u03ac\u03c3\u03b9\u03cc \u03c3\u03b1\u03c2" + }, "user": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 Growatt \u03c3\u03b1\u03c2" } } diff --git a/homeassistant/components/growatt_server/translations/nb.json b/homeassistant/components/growatt_server/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/pt-BR.json b/homeassistant/components/growatt_server/translations/pt-BR.json new file mode 100644 index 0000000000000..29222d32df4a0 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "no_plants": "Nenhuma planta foi encontrada nesta conta" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "plant": { + "data": { + "plant_id": "Planta" + }, + "title": "Selecione sua planta" + }, + "user": { + "data": { + "name": "Nome", + "password": "Senha", + "url": "URL", + "username": "Usu\u00e1rio" + }, + "title": "Insira suas informa\u00e7\u00f5es do Growatt" + } + } + }, + "title": "Servidor Growatt" +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/sk.json b/homeassistant/components/growatt_server/translations/sk.json new file mode 100644 index 0000000000000..d2e4793a68b4d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 9957e4602bd23..1efdc685a2411 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": ["gstreamer-player==1.1.2"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["gsp"] } diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 4de42e3190afa..8dfb37ad55170 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": ["pygtfs==0.1.6"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pygtfs"] } diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 55af1619da597..b971a428a76df 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, cast +from typing import cast from aioguardian import Client from aioguardian.errors import GuardianError @@ -12,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_ID, - ATTR_ENTITY_ID, CONF_DEVICE_ID, CONF_FILENAME, CONF_IP_ADDRESS, @@ -22,11 +21,7 @@ ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -71,41 +66,26 @@ SERVICE_NAME_UPGRADE_FIRMWARE, ) -SERVICE_BASE_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_id, - } - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +SERVICE_BASE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + } ) -SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(CONF_UID): cv.string, - } - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(CONF_UID): cv.string, + } ) -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.All( - cv.deprecated(ATTR_ENTITY_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, ) @@ -115,14 +95,6 @@ @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" - if ATTR_ENTITY_ID in call.data: - entity_registry = er.async_get(hass) - entity_registry_entry = entity_registry.async_get(call.data[ATTR_ENTITY_ID]) - if TYPE_CHECKING: - assert entity_registry_entry - assert entity_registry_entry.config_entry_id - return entity_registry_entry.config_entry_id - device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 90e33a82452a8..7ba3e1971f406 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -20,5 +20,6 @@ "hostname": "guardian*", "macaddress": "30AEA4*" } - ] + ], + "loggers": ["aioguardian"] } diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index b6d105dc21a16..8173f4d7fa04f 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -1,11 +1,23 @@ { "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", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Guardian;" + }, + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Guardian;" } } } diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json new file mode 100644 index 0000000000000..f0996e6b84ec7 --- /dev/null +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar" + }, + "step": { + "discovery_confirm": { + "description": "Deseja configurar este dispositivo Guardian?" + }, + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "description": "Configure um dispositivo local Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "Deseja configurar este dispositivo Guardian?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sk.json b/homeassistant/components/guardian/translations/sk.json new file mode 100644 index 0000000000000..b41d6edbd4b1e --- /dev/null +++ b/homeassistant/components/guardian/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 4967a6e87ba81..fdf170e2ede5f 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/habitica", "requirements": ["habitipy==0.2.0"], "codeowners": ["@ASMfreaK", "@leikoilja"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["habitipy", "plumbum"] } diff --git a/homeassistant/components/habitica/translations/el.json b/homeassistant/components/habitica/translations/el.json index 45e87d4625bd9..43257c239a954 100644 --- a/homeassistant/components/habitica/translations/el.json +++ b/homeassistant/components/habitica/translations/el.json @@ -1,10 +1,16 @@ { "config": { + "error": { + "invalid_credentials": "\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": { "user": { "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "api_user": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 API \u03c4\u03b7\u03c2 Habitica", - "name": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c4\u03b7\u03c2 Habitica. \u0398\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd" + "name": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c4\u03b7\u03c2 Habitica. \u0398\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" }, "description": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Habitica \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03ba\u03b1\u03b9 \u03c4\u03c9\u03bd \u03b5\u03c1\u03b3\u03b1\u03c3\u03b9\u03ce\u03bd \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03b1\u03c2. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf api_id \u03ba\u03b1\u03b9 \u03c4\u03bf api_key \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf https://habitica.com/user/settings/api." } diff --git a/homeassistant/components/habitica/translations/pt-BR.json b/homeassistant/components/habitica/translations/pt-BR.json new file mode 100644 index 0000000000000..cbdb6e3453fc3 --- /dev/null +++ b/homeassistant/components/habitica/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "api_user": "ID de usu\u00e1rio da API do Habitica", + "name": "Substitua o nome de usu\u00e1rio do Habitica. Ser\u00e1 usado para chamadas de servi\u00e7o", + "url": "URL" + }, + "description": "Conecte seu perfil do Habitica para permitir o monitoramento do perfil e das tarefas do seu usu\u00e1rio. Observe que api_id e api_key devem ser obtidos em https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/sk.json b/homeassistant/components/habitica/translations/sk.json new file mode 100644 index 0000000000000..bcfc3880d99cc --- /dev/null +++ b/homeassistant/components/habitica/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_credentials": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index a4c338aa632a7..983dc60414a64 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": ["hangups==0.4.17"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["hangups", "urwid"] } diff --git a/homeassistant/components/hangouts/translations/el.json b/homeassistant/components/hangouts/translations/el.json index 63c8a6ebc4306..c4e46912a5162 100644 --- a/homeassistant/components/hangouts/translations/el.json +++ b/homeassistant/components/hangouts/translations/el.json @@ -1,5 +1,9 @@ { "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).", @@ -13,6 +17,11 @@ "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" } diff --git a/homeassistant/components/hangouts/translations/pt-BR.json b/homeassistant/components/hangouts/translations/pt-BR.json index 3f8fd23b07c9c..c60d9d8ec4702 100644 --- a/homeassistant/components/hangouts/translations/pt-BR.json +++ b/homeassistant/components/hangouts/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Hangouts do Google j\u00e1 est\u00e1 configurado.", - "unknown": "Ocorreu um erro desconhecido." + "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.", @@ -12,7 +12,7 @@ "step": { "2fa": { "data": { - "2fa": "Pin 2FA" + "2fa": "C\u00f3digo 2FA" }, "description": "Vazio", "title": "Autentica\u00e7\u00e3o de 2 Fatores" @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "C\u00f3digo de Autoriza\u00e7\u00e3o (requerido para autentica\u00e7\u00e3o manual)", - "email": "Endere\u00e7o de e-mail", + "email": "Email", "password": "Senha" }, "description": "Vazio", diff --git a/homeassistant/components/hangouts/translations/sk.json b/homeassistant/components/hangouts/translations/sk.json new file mode 100644 index 0000000000000..45123261c43d9 --- /dev/null +++ b/homeassistant/components/hangouts/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index a7f4fffa4d682..8a029ae63392e 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": ["hkavr==0.0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["hkavr"] } diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 4ec610f1f7570..4e109ae95a7f7 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -94,7 +94,7 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi hass.config_entries.async_update_entry(entry, options=options) -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" async_dispatcher_send( hass, f"{HARMONY_OPTIONS_UPDATE}-{entry.unique_id}", entry.options diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 948178901609d..ab5848a1fb766 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -18,5 +18,6 @@ ], "dependencies": ["remote", "switch"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aioharmony", "slixmpp"] } diff --git a/homeassistant/components/harmony/translations/el.json b/homeassistant/components/harmony/translations/el.json index 9b31f1615f77a..c8483acd34bc4 100644 --- a/homeassistant/components/harmony/translations/el.json +++ b/homeassistant/components/harmony/translations/el.json @@ -1,9 +1,35 @@ { "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" + }, "flow_title": "{name}", "step": { "link": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Logitech Harmony Hub" + }, + "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 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\u0397 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03c1\u03b1\u03c3\u03c4\u03b7\u03c1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03b5\u03af \u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03bc\u03af\u03b1.", + "delay_secs": "\u0397 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae\u03c2 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ce\u03bd." + }, + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c4\u03bf\u03c5 Harmony Hub" } } } diff --git a/homeassistant/components/harmony/translations/pt-BR.json b/homeassistant/components/harmony/translations/pt-BR.json index 7fe3f58cad606..4b3f264ddebab 100644 --- a/homeassistant/components/harmony/translations/pt-BR.json +++ b/homeassistant/components/harmony/translations/pt-BR.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Voc\u00ea quer configurar o {name} ({host})?", @@ -15,6 +15,7 @@ }, "user": { "data": { + "host": "Nome do host", "name": "Nome do Hub" }, "title": "Configura\u00e7\u00e3o do Logitech Harmony Hub" @@ -27,7 +28,8 @@ "data": { "activity": "A atividade padr\u00e3o a ser executada quando nenhuma for especificada.", "delay_secs": "O atraso entre o envio de comandos." - } + }, + "description": "Ajustar as op\u00e7\u00f5es do Harmony Hub" } } } diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 434c95b03b2dd..b5549c1b5e4f2 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -125,6 +125,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( { + vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), } diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 6b77a180c0972..6186f222183c8 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -87,6 +87,11 @@ backup_partial: name: Create a partial backup. description: Create a partial backup. fields: + homeassistant: + name: Home Assistant settings + description: Backup Home Assistant settings + selector: + boolean: addons: name: Add-ons description: Optional list of add-on slugs. diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 960dc53b5eaa2..941c3601bea4b 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -2,7 +2,11 @@ "system_health": { "info": { "disk_total": "\u0414\u0438\u0441\u043a \u043e\u0431\u0449\u043e", - "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a" + "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a", + "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker", + "installed_addons": "\u0418\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0438 \u0434\u043e\u0431\u0430\u0432\u043a\u0438", + "supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Supervisor", + "update_channel": "\u041a\u0430\u043d\u0430\u043b \u0437\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json new file mode 100644 index 0000000000000..b157725600d5a --- /dev/null +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "board": "Borda", + "disk_total": "Total do disco", + "disk_used": "Disco usado", + "docker_version": "Vers\u00e3o do Docker", + "healthy": "Saud\u00e1vel", + "host_os": "Sistema Operacional Host", + "installed_addons": "Add-ons instalados", + "supervisor_api": "API do supervisor", + "supervisor_version": "Vers\u00e3o do Supervisor", + "supported": "Suportado", + "update_channel": "Atualizar canal", + "version_api": "API de vers\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 08797541eed58..ff2411db35afe 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": ["pyCEC==0.5.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pycec"] } diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 772171660529b..8b783e40758ee 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": ["heatmiserV3==1.1.18"], "codeowners": ["@andylockran"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["heatmiserV3"] } diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index bbe611c1db9de..dbd66e28307bd 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -331,8 +331,7 @@ async def async_update_groups(self, event, data=None): heos_const.EVENT_CONNECTED, SIGNAL_HEOS_PLAYER_ADDED, ): - groups = await self.async_get_group_membership() - if groups: + if groups := await self.async_get_group_membership(): self._group_membership = groups _LOGGER.debug("Groups updated due to change event") # Let players know to update diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 94794bf536d84..ba7f2e3664cb6 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -10,5 +10,6 @@ } ], "codeowners": ["@andrewsayre"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyheos"] } diff --git a/homeassistant/components/heos/translations/el.json b/homeassistant/components/heos/translations/el.json new file mode 100644 index 0000000000000..6eb00ad272388 --- /dev/null +++ b/homeassistant/components/heos/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "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" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \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 \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03b9\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Heos (\u03ba\u03b1\u03c4\u03ac \u03c0\u03c1\u03bf\u03c4\u03af\u03bc\u03b7\u03c3\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7\u03c2 \u03bc\u03b5 \u03ba\u03b1\u03bb\u03ce\u03b4\u03b9\u03bf \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf).", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Heos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/pt-BR.json b/homeassistant/components/heos/translations/pt-BR.json index 328264d3adfb5..767cd5d5fb3dc 100644 --- a/homeassistant/components/heos/translations/pt-BR.json +++ b/homeassistant/components/heos/translations/pt-BR.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Por favor, digite o nome do host ou o endere\u00e7o IP de um dispositivo Heos (de prefer\u00eancia para conex\u00f5es conectadas por cabo \u00e0 sua rede).", "title": "Conecte-se a Heos" diff --git a/homeassistant/components/heos/translations/uk.json b/homeassistant/components/heos/translations/uk.json index c0a5fdf04bf94..8ea94d7a04504 100644 --- a/homeassistant/components/heos/translations/uk.json +++ b/homeassistant/components/heos/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" diff --git a/homeassistant/components/heos/translations/zh-Hant.json b/homeassistant/components/heos/translations/zh-Hant.json index fe3e8fb7b432f..8a452d98d0312 100644 --- a/homeassistant/components/heos/translations/zh-Hant.json +++ b/homeassistant/components/heos/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 9a3e8bd482717..b620153bba7b0 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": ["herepy==2.0.0"], "codeowners": ["@eifinger"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["herepy"] } diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index a8f8940114823..7209ee0002407 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": ["pyhik==0.3.0"], "codeowners": ["@mezz64"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyhik"] } diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index 61c629655cecf..84f7f4e28e121 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": ["hikvision==0.4"], "codeowners": ["@fbradyirl"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["hikvision"] } diff --git a/homeassistant/components/hisense_aehw4a1/config_flow.py b/homeassistant/components/hisense_aehw4a1/config_flow.py index 06eebb4594883..8fd651d178236 100644 --- a/homeassistant/components/hisense_aehw4a1/config_flow.py +++ b/homeassistant/components/hisense_aehw4a1/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Hisense AEH-W4A1 integration.""" from pyaehw4a1.aehw4a1 import AehW4a1 +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DOMAIN -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" aehw4a1_ip_addresses = await AehW4a1().discovery() return len(aehw4a1_ip_addresses) > 0 diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json index 514ee712710b8..d0e669783d710 100644 --- a/homeassistant/components/hisense_aehw4a1/manifest.json +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", "requirements": ["pyaehw4a1==0.3.9"], "codeowners": ["@bannhead"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyaehw4a1"] } diff --git a/homeassistant/components/hisense_aehw4a1/translations/el.json b/homeassistant/components/hisense_aehw4a1/translations/el.json new file mode 100644 index 0000000000000..dbe14be152655 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Hisense AEH-W4A1;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/pt-BR.json b/homeassistant/components/hisense_aehw4a1/translations/pt-BR.json new file mode 100644 index 0000000000000..1b98cc41a7ae6 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja configurar o Hisense AEH-W4A1?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/uk.json b/homeassistant/components/hisense_aehw4a1/translations/uk.json index 900882513d512..d7da075345aee 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/uk.json +++ b/homeassistant/components/hisense_aehw4a1/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json index e08a2c5f6df0c..49eae73f25e19 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json +++ b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 4f0520eaa72f6..4a0ad577f90f8 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -36,8 +36,7 @@ async def async_setup_entry( """Set up Hive thermostat based on a config entry.""" hive = hass.data[DOMAIN][entry.entry_id] - devices = hive.session.deviceList.get("alarm_control_panel") - if devices: + if devices := hive.session.deviceList.get("alarm_control_panel"): async_add_entities( [HiveAlarmControlPanelEntity(hive, dev) for dev in devices], True ) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 5f23eef642b12..2ce097fb8cc04 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -10,5 +10,6 @@ "@Rendili", "@KJonline" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["apyhiveapi"] } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/nb.json b/homeassistant/components/hive/translations/nb.json new file mode 100644 index 0000000000000..a9f534742c517 --- /dev/null +++ b/homeassistant/components/hive/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/pt-BR.json b/homeassistant/components/hive/translations/pt-BR.json new file mode 100644 index 0000000000000..5f5f7e857c2f4 --- /dev/null +++ b/homeassistant/components/hive/translations/pt-BR.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown_entry": "N\u00e3o foi poss\u00edvel encontrar a entrada existente." + }, + "error": { + "invalid_code": "Falha ao entrar no Hive. Seu c\u00f3digo de autentica\u00e7\u00e3o de dois fatores estava incorreto.", + "invalid_password": "Falha ao entrar no Hive. Senha incorreta. Por favor tente novamente.", + "invalid_username": "Falha ao entrar no Hive. Seu endere\u00e7o de e-mail n\u00e3o \u00e9 reconhecido.", + "no_internet_available": "\u00c9 necess\u00e1ria uma conex\u00e3o com a Internet para se conectar ao Hive.", + "unknown": "Erro inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dois fatores" + }, + "description": "Digite seu c\u00f3digo de autentica\u00e7\u00e3o Hive. \n\n Insira o c\u00f3digo 0000 para solicitar outro c\u00f3digo.", + "title": "Autentica\u00e7\u00e3o de dois fatores do Hive." + }, + "reauth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Insira novamente suas informa\u00e7\u00f5es de login do Hive.", + "title": "Login do Hive" + }, + "user": { + "data": { + "password": "Senha", + "scan_interval": "Intervalo de escaneamento (segundos)", + "username": "Usu\u00e1rio" + }, + "description": "Insira suas informa\u00e7\u00f5es de login e configura\u00e7\u00e3o do Hive.", + "title": "Login do Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Intervalo de escaneamento (segundos)" + }, + "description": "Atualize o intervalo de varredura para pesquisar dados com mais frequ\u00eancia.", + "title": "Op\u00e7\u00f5es para o Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/sk.json b/homeassistant/components/hive/translations/sk.json new file mode 100644 index 0000000000000..c2f015fe339a0 --- /dev/null +++ b/homeassistant/components/hive/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 1bd0a73b7ab97..12638679f5a7c 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -5,5 +5,6 @@ "requirements": ["hlk-sw16==0.0.9"], "codeowners": ["@jameshilliard"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["hlk_sw16"] } diff --git a/homeassistant/components/hlk_sw16/translations/cs.json b/homeassistant/components/hlk_sw16/translations/cs.json index 1801c0d3b92f6..a4bad4b7c9f58 100644 --- a/homeassistant/components/hlk_sw16/translations/cs.json +++ b/homeassistant/components/hlk_sw16/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed ji\u017e je nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { "cannot_connect": "Nelze se p\u0159ipojit", diff --git a/homeassistant/components/hlk_sw16/translations/el.json b/homeassistant/components/hlk_sw16/translations/el.json new file mode 100644 index 0000000000000..877622243c8a8 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/el.json @@ -0,0 +1,21 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/pt-BR.json b/homeassistant/components/hlk_sw16/translations/pt-BR.json new file mode 100644 index 0000000000000..93beddb92a851 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/sk.json b/homeassistant/components/hlk_sw16/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index b9a4f8e6ddb63..5667d5399021e 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@DavidMStraub"], "requirements": ["homeconnect==0.6.3"], "config_flow": true, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["homeconnect"] } diff --git a/homeassistant/components/home_connect/translations/el.json b/homeassistant/components/home_connect/translations/el.json new file mode 100644 index 0000000000000..b82ab8fa03be2 --- /dev/null +++ b/homeassistant/components/home_connect/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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} )" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/pt-BR.json b/homeassistant/components/home_connect/translations/pt-BR.json index ff8e13aed1feb..54cd4aece0bb2 100644 --- a/homeassistant/components/home_connect/translations/pt-BR.json +++ b/homeassistant/components/home_connect/translations/pt-BR.json @@ -1,7 +1,16 @@ { "config": { "abort": { - "missing_configuration": "O componente Home Connect n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + "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})" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } } } } \ No newline at end of file diff --git a/homeassistant/components/home_connect/translations/sk.json b/homeassistant/components/home_connect/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/home_connect/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/manifest.json b/homeassistant/components/home_plus_control/manifest.json index edbf0147e145a..30ef34b6b3444 100644 --- a/homeassistant/components/home_plus_control/manifest.json +++ b/homeassistant/components/home_plus_control/manifest.json @@ -6,5 +6,6 @@ "requirements": ["homepluscontrol==0.0.5"], "dependencies": ["http"], "codeowners": ["@chemaaa"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["homepluscontrol"] } diff --git a/homeassistant/components/home_plus_control/translations/el.json b/homeassistant/components/home_plus_control/translations/el.json new file mode 100644 index 0000000000000..724dafff28fee --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/el.json @@ -0,0 +1,21 @@ +{ + "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_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", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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} )", + "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." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + }, + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 Legrand Home+" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/pt-BR.json b/homeassistant/components/home_plus_control/translations/pt-BR.json new file mode 100644 index 0000000000000..3b7340be7c7e8 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + }, + "title": "Legrand Home+ Control" +} \ 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 new file mode 100644 index 0000000000000..97ac5f20ed2b0 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/zh-Hant.json b/homeassistant/components/home_plus_control/translations/zh-Hant.json index 0faa311028720..da55be65f0473 100644 --- a/homeassistant/components/home_plus_control/translations/zh-Hant.json +++ b/homeassistant/components/home_plus_control/translations/zh-Hant.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "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})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/homeassistant/translations/el.json b/homeassistant/components/homeassistant/translations/el.json index 5f2110e450936..616ad96a86719 100644 --- a/homeassistant/components/homeassistant/translations/el.json +++ b/homeassistant/components/homeassistant/translations/el.json @@ -10,6 +10,7 @@ "os_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "python_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 Python", "timezone": "\u0396\u03ce\u03bd\u03b7 \u03ce\u03c1\u03b1\u03c2", + "user": "\u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2", "version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7", "virtualenv": "\u0395\u03b9\u03ba\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd" } diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json new file mode 100644 index 0000000000000..f30a1775e0eb3 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "arch": "Arquitetura da CPU", + "dev": "Desenvolvimento", + "docker": "Docker", + "hassio": "Supervisor", + "installation_type": "Tipo de instala\u00e7\u00e3o", + "os_name": "Fam\u00edlia de sistemas operacionais", + "os_version": "Vers\u00e3o do sistema operacional", + "python_version": "Vers\u00e3o do Python", + "timezone": "Fuso hor\u00e1rio", + "user": "Usu\u00e1rio", + "version": "Vers\u00e3o", + "virtualenv": "Ambiente Virtual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 843cc6ea642c4..4cd0940adb726 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -313,7 +313,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" if entry.source == SOURCE_IMPORT: return diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d8d5a16ac5076..d348b4c1f42f8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,4 +1,6 @@ """Extend the basic Accessory and Bridge functions.""" +from __future__ import annotations + import logging from pyhap.accessory import Accessory, Bridge @@ -90,7 +92,7 @@ TYPE_SWITCH: "Switch", TYPE_VALVE: "Valve", } -TYPES = Registry() +TYPES: Registry[str, type[HomeAccessory]] = Registry() def get_accessory(hass, driver, state, aid, config): # noqa: C901 @@ -119,14 +121,10 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 elif state.domain == "cover": device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if ( - device_class - in ( - cover.CoverDeviceClass.GARAGE, - cover.CoverDeviceClass.GATE, - ) - and features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE) - ): + if device_class in ( + cover.CoverDeviceClass.GARAGE, + cover.CoverDeviceClass.GATE, + ) and features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = "GarageDoorOpener" elif ( device_class == cover.CoverDeviceClass.WINDOW diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 7eca4ae4c661e..ed7b3d6b293da 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -215,6 +215,7 @@ CHAR_TARGET_DOOR_STATE = "TargetDoorState" CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" CHAR_TARGET_POSITION = "TargetPosition" +CHAR_TARGET_FAN_STATE = "TargetFanState" CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER = "TargetHumidifierDehumidifierState" CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity" CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 4b54468e092b6..bde540d637225 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -8,10 +8,11 @@ "PyQRCode==1.2.1", "base36==0.1.1" ], - "dependencies": ["http", "camera", "ffmpeg", "network"], - "after_dependencies": ["zeroconf"], + "dependencies": ["ffmpeg", "http", "network"], + "after_dependencies": ["camera", "zeroconf"], "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyhap"] } diff --git a/homeassistant/components/homekit/translations/bg.json b/homeassistant/components/homekit/translations/bg.json index 4e5677f124a3f..a7aa5bb792b9c 100644 --- a/homeassistant/components/homekit/translations/bg.json +++ b/homeassistant/components/homekit/translations/bg.json @@ -1,8 +1,25 @@ { "options": { "step": { + "accessory": { + "data": { + "entities": "\u041e\u0431\u0435\u043a\u0442" + } + }, + "exclude": { + "data": { + "entities": "\u041e\u0431\u0435\u043a\u0442\u0438" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043e\u0431\u0435\u043a\u0442\u0438\u0442\u0435, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u0438" + }, + "include": { + "data": { + "entities": "\u041e\u0431\u0435\u043a\u0442\u0438" + } + }, "include_exclude": { "data": { + "entities": "\u041e\u0431\u0435\u043a\u0442\u0438", "mode": "\u0420\u0435\u0436\u0438\u043c" } }, diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json index 24052b026bdde..632d9aecd764a 100644 --- a/homeassistant/components/homekit/translations/el.json +++ b/homeassistant/components/homekit/translations/el.json @@ -1,4 +1,22 @@ { + "config": { + "abort": { + "port_name_in_use": "\u0388\u03bd\u03b1 \u03b5\u03be\u03ac\u03c1\u03c4\u03b7\u03bc\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ae \u03b8\u03cd\u03c1\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af." + }, + "step": { + "pairing": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03c3\u03c4\u03b9\u03c2 \"\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \"\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 HomeKit\".", + "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 HomeKit" + }, + "user": { + "data": { + "include_domains": "\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c4\u03bf\u03bc\u03b5\u03af\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd. \u0398\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c3\u03c4\u03bf\u03bd \u03c4\u03bf\u03bc\u03ad\u03b1, \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03b9\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2. \u0398\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03ae \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 HomeKit \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd tv, \u03c4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf \u03bc\u03b5 \u03b2\u03ac\u03c3\u03b7 \u03c4\u03b7 \u03b4\u03c1\u03b1\u03c3\u03c4\u03b7\u03c1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1, \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1.", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bc\u03b5\u03af\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" + } + } + }, "options": { "step": { "accessory": { @@ -16,10 +34,17 @@ "title": "\u03a0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" }, "cameras": { + "data": { + "camera_audio": "\u039a\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03c5\u03bd \u03ae\u03c7\u03bf", + "camera_copy": "\u039a\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03c5\u03bd \u03b5\u03b3\u03b3\u03b5\u03bd\u03b5\u03af\u03c2 \u03c1\u03bf\u03ad\u03c2 H.264" + }, "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03c5\u03bd \u03b5\u03b3\u03b3\u03b5\u03bd\u03b5\u03af\u03c2 \u03c1\u03bf\u03ad\u03c2 H.264. \u0395\u03ac\u03bd \u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03b9 \u03c1\u03bf\u03ae H.264, \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b8\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c3\u03c7\u03b7\u03bc\u03b1\u03c4\u03af\u03c3\u03b5\u03b9 \u03c4\u03bf \u03b2\u03af\u03bd\u03c4\u03b5\u03bf \u03c3\u03b5 H.264 \u03b3\u03b9\u03b1 \u03c4\u03bf HomeKit. \u0397 \u03bc\u03b5\u03c4\u03b1\u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03bc\u03b9\u03b1 \u03b1\u03c0\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03ae CPU \u03ba\u03b1\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03af\u03b8\u03b1\u03bd\u03bf \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03b5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03bf\u03bd\u03ae\u03c2 \u03c0\u03bb\u03b1\u03ba\u03ad\u03c4\u03b1\u03c2.", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2" }, "exclude": { + "data": { + "entities": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2" + }, "description": "\u0398\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \"{domains}\" \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b5\u03be\u03b1\u03b9\u03c1\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03b9\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2.", "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03be\u03b1\u03b9\u03c1\u03b5\u03b8\u03bf\u03cd\u03bd" }, @@ -32,16 +57,25 @@ }, "include_exclude": { "data": { - "entities": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2" + "entities": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1, \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 include bridge, \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03bc\u03ad\u03b1, \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bf\u03cd\u03bd \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03bf\u03cd \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1\u03c2, \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03bc\u03ad\u03b1 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03c4\u03b5\u03af. \u0393\u03b9\u03b1 \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7, \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03ad\u03bd\u03b1 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03cc \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 HomeKit \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd tv, \u03c4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf \u03bc\u03b5 \u03b2\u03ac\u03c3\u03b7 \u03c4\u03b7 \u03b4\u03c1\u03b1\u03c3\u03c4\u03b7\u03c1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1, \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" }, "init": { "data": { + "domains": "\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd", + "include_domains": "\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd", "include_exclude_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03af\u03bb\u03b7\u03c8\u03b7\u03c2", "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 HomeKit" - } + }, + "description": "\u03a4\u03bf HomeKit \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03ba\u03b8\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 \u03ae \u03ad\u03bd\u03b1 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03bc\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1. \u0397 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03bf\u03cd\u03bd \u03c3\u03c9\u03c3\u03c4\u03ac \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd \u03bc\u03b5 \u03c4\u03b7\u03bd \u03ba\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 TV. \u039f\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c3\u03c4\u03bf \"\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd\" \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03c3\u03c4\u03bf HomeKit. \u0398\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ae \u03b8\u03b1 \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bf\u03b8\u03cc\u03bd\u03b7.", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bc\u03b5\u03af\u03c2." + }, + "yaml": { + "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 YAML", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd HomeKit" } } } diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 6c4a3e36836ff..7de4c531d6aa4 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -22,7 +22,8 @@ "accessory": { "data": { "entities": "Entit\u00e9" - } + }, + "title": "S\u00e9lectionnez l'entit\u00e9 pour l'accessoire" }, "advanced": { "data": { @@ -44,6 +45,7 @@ "data": { "entities": "Entit\u00e9s" }, + "description": "Toutes les entit\u00e9s \u00ab {domains}\u00a0\u00bb seront incluses, \u00e0 l'exception des entit\u00e9s exclues et des entit\u00e9s cat\u00e9goris\u00e9es.", "title": "S\u00e9lectionnez les entit\u00e9s \u00e0 exclure" }, "include": { @@ -66,10 +68,10 @@ "domains": "Domaines \u00e0 inclure", "include_domains": "Domaines \u00e0 inclure", "include_exclude_mode": "Mode d'inclusion", - "mode": "Mode" + "mode": "Mode HomeKit" }, "description": "Les entit\u00e9s des \u00abdomaines \u00e0 inclure\u00bb seront pont\u00e9es vers HomeKit. Vous pourrez s\u00e9lectionner les entit\u00e9s \u00e0 exclure de cette liste sur l'\u00e9cran suivant.", - "title": "S\u00e9lectionnez les domaines \u00e0 relier." + "title": "S\u00e9lectionnez le mode et les domaines." }, "yaml": { "description": "Cette entr\u00e9e est contr\u00f4l\u00e9e via YAML", diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index cadbd1aa4b899..22f3515b49707 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -18,6 +18,16 @@ "cameras": { "title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05de\u05e6\u05dc\u05de\u05d4" }, + "exclude": { + "data": { + "entities": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea" + } + }, + "include": { + "data": { + "entities": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea" + } + }, "include_exclude": { "data": { "mode": "\u05de\u05e6\u05d1" diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index 0f9da18f7a8f8..5faedff893b6c 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domain yang disertakan" }, - "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV, remote berbasis aktivitas, kunci, dan kamera.", + "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan kecuali entitas yang dikategorikan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV, remote berbasis aktivitas, kunci, dan kamera.", "title": "Pilih domain yang akan disertakan" } } @@ -68,10 +68,10 @@ "domains": "Domain yang disertakan", "include_domains": "Domain yang disertakan", "include_exclude_mode": "Mode Penyertaan", - "mode": "Mode" + "mode": "Mode HomeKit" }, "description": "HomeKit dapat dikonfigurasi untuk memaparkakan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", - "title": "Pilih domain yang akan disertakan." + "title": "Pilih mode dan domain." }, "yaml": { "description": "Entri ini dikontrol melalui YAML", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 46c1a8063fdc7..186bf989a4ecd 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -31,7 +31,7 @@ "devices": "Dispositivi (Attivatori)" }, "description": "Gli interruttori programmabili vengono creati per ogni dispositivo selezionato. Quando si attiva un trigger del dispositivo, HomeKit pu\u00f2 essere configurato per eseguire un'automazione o una scena.", - "title": "Configurazione Avanzata" + "title": "Configurazione avanzata" }, "cameras": { "data": { diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 05f6c83ad73a3..6d6b441484fc8 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -45,12 +45,14 @@ "data": { "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" }, + "description": "\u9664\u5916\u3055\u308c\u3066\u3044\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3068\u5206\u985e\u3055\u308c\u3066\u3044\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u304d\u3001\u3059\u3079\u3066\u306e \u201c{domains}\u201d \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002", "title": "\u9664\u5916\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" }, "include": { "data": { "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" }, + "description": "\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u3059\u3079\u3066\u306e \u201c{domains}\u201d \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" }, "include_exclude": { diff --git a/homeassistant/components/homekit/translations/nb.json b/homeassistant/components/homekit/translations/nb.json new file mode 100644 index 0000000000000..0015010c63c29 --- /dev/null +++ b/homeassistant/components/homekit/translations/nb.json @@ -0,0 +1,16 @@ +{ + "options": { + "step": { + "exclude": { + "data": { + "entities": "Entiteter" + } + }, + "include": { + "data": { + "entities": "Entiteter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 4b0a4a64a71b0..9788f4904b6b8 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -67,10 +67,11 @@ "data": { "domains": "Domeinen om op te nemen", "include_domains": "Domeinen om op te nemen", - "mode": "modus" + "include_exclude_mode": "Inclusiemodus", + "mode": "HomeKit-modus" }, "description": "HomeKit kan worden geconfigureerd om een brug of een enkel accessoire te tonen. In de accessoiremodus kan slechts \u00e9\u00e9n entiteit worden gebruikt. De accessoiremodus is vereist om mediaspelers met de tv-apparaatklasse correct te laten werken. Entiteiten in de \"Op te nemen domeinen\" zullen worden blootgesteld aan HomeKit. U kunt op het volgende scherm selecteren welke entiteiten u wilt opnemen of uitsluiten van deze lijst.", - "title": "Selecteer domeinen om zichtbaar te maken." + "title": "Selecteer modus en domeinen." }, "yaml": { "description": "Deze invoer wordt beheerd via YAML", diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 52dc19b164db6..745f07bdadbc2 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domeny do uwzgl\u0119dnienia" }, - "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera, pilota na bazie aktywno\u015bci, zamka oraz kamery.", + "description": "Wybierz domeny do uwzgl\u0119dnienia. Wszystkie wspierane encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione z wyj\u0105tkiem skategoryzowanych encji. W trybie akcesorium, oddzielna instancja HomeKit zostanie utworzona dla ka\u017cdego tv media playera, pilota na bazie aktywno\u015bci, zamka oraz kamery.", "title": "Wybierz uwzgl\u0119dniane domeny" } } @@ -42,6 +42,9 @@ "title": "Konfiguracja kamery" }, "exclude": { + "data": { + "entities": "Encje" + }, "description": "Wszystkie encje \"{domains}\" zostan\u0105 uwzgl\u0119dnione z wyj\u0105tkiem encji wykluczonych oraz encji skategoryzowanych.", "title": "Wybierz encje do wykluczenia" }, @@ -62,12 +65,13 @@ }, "init": { "data": { + "domains": "Domeny do uwzgl\u0119dnienia", "include_domains": "Domeny do uwzgl\u0119dnienia", - "include_exclude_mode": "Tryb w\u0142\u0105czania", - "mode": "Tryb" + "include_exclude_mode": "Tryb dodawania", + "mode": "Tryb HomeKit" }, "description": "HomeKit mo\u017ce by\u0107 skonfigurowany, by pokaza\u0142 mostek lub pojedyncze akcesorium. W trybie \"Akcesorium\", tylko pojedyncza encja mo\u017ce by\u0107 u\u017cyta. Tryb ten jest wymagany w przypadku odtwarzaczy multimedialnych z klas\u0105 urz\u0105dzenia TV, by dzia\u0142a\u0142 prawid\u0142owo. Encje w \"uwzgl\u0119dnionych domenach\" b\u0119d\u0105 widoczne w HomeKit. B\u0119dziesz m\u00f3g\u0142 wybra\u0107, kt\u00f3re encje maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z tej listy na nast\u0119pnym ekranie.", - "title": "Domeny do uwzgl\u0119dnienia." + "title": "Wybierz tryb i domeny." }, "yaml": { "description": "Ten wpis jest kontrolowany przez YAML", diff --git a/homeassistant/components/homekit/translations/pt-BR.json b/homeassistant/components/homekit/translations/pt-BR.json index 1ef802a92023b..ad8aab120f4e2 100644 --- a/homeassistant/components/homekit/translations/pt-BR.json +++ b/homeassistant/components/homekit/translations/pt-BR.json @@ -1,11 +1,81 @@ { + "config": { + "abort": { + "port_name_in_use": "Um acess\u00f3rio ou ponte com o mesmo nome ou porta j\u00e1 est\u00e1 configurado." + }, + "step": { + "pairing": { + "description": "Para concluir o emparelhamento, siga as instru\u00e7\u00f5es em \"Emparelhamento do HomeKit\" > \"Notifica\u00e7\u00f5es\".", + "title": "Emparelhar HomeKit" + }, + "user": { + "data": { + "include_domains": "Dom\u00ednios para incluir" + }, + "description": "Escolha os dom\u00ednios a serem inclu\u00eddos. Todas as entidades apoiadas no dom\u00ednio ser\u00e3o inclu\u00eddas, exceto para entidades categorizadas. Uma inst\u00e2ncia separada do HomeKit no modo acess\u00f3rio ser\u00e1 criada para cada leitor de m\u00eddia de TV, controle remoto, bloqueio e c\u00e2mera baseados em atividades.", + "title": "Selecione os dom\u00ednios a serem inclu\u00eddos" + } + } + }, "options": { "step": { + "accessory": { + "data": { + "entities": "Entidades" + }, + "title": "Selecione a entidade para o acess\u00f3rio" + }, "advanced": { + "data": { + "auto_start": "Autostart (desabilite se voc\u00ea estiver chamando o servi\u00e7o homekit.start manualmente)", + "devices": "Dispositivos (gatilhos)" + }, + "description": "Os interruptores program\u00e1veis s\u00e3o criados para cada dispositivo selecionado. Quando um dispositivo dispara, o HomeKit pode ser configurado para executar uma automa\u00e7\u00e3o ou cena.", "title": "Configura\u00e7\u00e3o avan\u00e7ada" }, "cameras": { - "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." + "data": { + "camera_audio": "C\u00e2meras que suportam \u00e1udio", + "camera_copy": "C\u00e2meras que suportam streams H.264 nativos" + }, + "description": "Verifique todas as c\u00e2meras que suportam fluxos H.264 nativos. Se a c\u00e2mera n\u00e3o emitir um fluxo H.264, o sistema transcodificar\u00e1 o v\u00eddeo para H.264 para HomeKit. A transcodifica\u00e7\u00e3o requer uma CPU de alto desempenho e \u00e9 improv\u00e1vel que funcione em computadores de placa \u00fanica.", + "title": "Configura\u00e7\u00e3o da c\u00e2mera" + }, + "exclude": { + "data": { + "entities": "Entidades" + }, + "description": "Todas as {domains} \u201d ser\u00e3o inclu\u00eddas, exceto as entidades exclu\u00eddas e as entidades categorizadas.", + "title": "Selecione as entidades a serem exclu\u00eddas" + }, + "include": { + "data": { + "entities": "Entidades" + }, + "description": "Todas as entidades \"{domains}\" ser\u00e3o inclu\u00eddas, a menos que entidades espec\u00edficas sejam selecionadas.", + "title": "Selecione as entidades a serem inclu\u00eddas" + }, + "include_exclude": { + "data": { + "entities": "Entidades", + "mode": "Modo" + }, + "description": "Escolha as entidades a serem inclu\u00eddas. No modo acess\u00f3rio, apenas uma \u00fanica entidade est\u00e1 inclu\u00edda. No modo de incluir ponte, todas as entidades do dom\u00ednio ser\u00e3o inclu\u00eddas a menos que entidades espec\u00edficas sejam selecionadas. No modo de exclus\u00e3o da ponte, todas as entidades do dom\u00ednio ser\u00e3o inclu\u00eddas, exceto para as entidades exclu\u00eddas. Para melhor desempenho, um acess\u00f3rio HomeKit separado ser\u00e1 criado para cada leitor de m\u00eddia de TV, controle remoto, bloqueio e c\u00e2mera baseados em atividades.", + "title": "Selecione as entidades a serem inclu\u00eddas" + }, + "init": { + "data": { + "domains": "Dom\u00ednios a serem inclu\u00eddos", + "include_domains": "Dom\u00ednios para incluir", + "include_exclude_mode": "Modo de inclus\u00e3o", + "mode": "Modo HomeKit" + }, + "description": "O HomeKit pode ser configurado para expor uma ponte ou um \u00fanico acess\u00f3rio. No modo acess\u00f3rio, apenas uma \u00fanica entidade pode ser usada. O modo acess\u00f3rio \u00e9 necess\u00e1rio para que os players de m\u00eddia com a classe de dispositivo TV funcionem corretamente. As entidades nos \u201cDom\u00ednios a incluir\u201d ser\u00e3o inclu\u00eddas no HomeKit. Voc\u00ea poder\u00e1 selecionar quais entidades incluir ou excluir desta lista na pr\u00f3xima tela.", + "title": "Selecione o modo e os dom\u00ednios." + }, + "yaml": { + "description": "Esta entrada \u00e9 controlada via YAML", + "title": "Ajustar as op\u00e7\u00f5es do HomeKit" } } } diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 73875ea042333..3f6d005f0459b 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "\u4e00\u65e6 {name} \u51c6\u5907\u5c31\u7eea\uff0c\u5c31\u53ef\u4ee5\u5728\u201c\u901a\u77e5\u201d\u627e\u5230\u201cHomeKit \u6865\u63a5\u5668\u914d\u7f6e\u201d\u8fdb\u884c\u914d\u5bf9\u3002", + "description": "\u8bf7\u5728\u201c\u901a\u77e5\u201d\u4e2d\u627e\u5230\u201cHomeKit Pairing\u201d\uff0c\u8ddf\u968f\u6307\u5f15\u5b8c\u6210\u914d\u5bf9\u8fc7\u7a0b\u3002", "title": "\u4e0e HomeKit \u914d\u5bf9" }, "user": { @@ -19,6 +19,12 @@ }, "options": { "step": { + "accessory": { + "data": { + "entities": "\u5b9e\u4f53" + }, + "title": "\u9009\u62e9\u914d\u4ef6\u7684\u5b9e\u4f53" + }, "advanced": { "data": { "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u60a8\u624b\u52a8\u8c03\u7528 homekit.start \u670d\u52a1\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", @@ -35,18 +41,34 @@ "description": "\u67e5\u627e\u6240\u6709\u652f\u6301\u539f\u751f H.264 \u63a8\u6d41\u7684\u6444\u50cf\u673a\u3002\u5982\u679c\u6444\u50cf\u673a\u8f93\u51fa\u7684\u4e0d\u662f H.264 \u6d41\uff0c\u7cfb\u7edf\u4f1a\u5c06\u89c6\u9891\u8f6c\u7801\u4e3a H.264 \u4ee5\u4f9b HomeKit \u4f7f\u7528\u3002\u8f6c\u7801\u9700\u8981\u9ad8\u6027\u80fd\u7684 CPU\uff0c\u56e0\u6b64\u5728\u5f00\u53d1\u677f\u8ba1\u7b97\u673a\u4e0a\u5f88\u96be\u5b8c\u6210\u3002", "title": "\u6444\u50cf\u673a\u914d\u7f6e" }, + "exclude": { + "data": { + "entities": "\u5b9e\u4f53" + }, + "description": "\u9664\u4e86\u6307\u5b9a\u7684\u5b9e\u4f53\u548c\u5206\u7c7b\uff0c\u6240\u6709\u201c{domains}\u201d\u7c7b\u578b\u7684\u5b9e\u4f53\u90fd\u5c06\u88ab\u5305\u542b\u3002", + "title": "\u9009\u62e9\u8981\u6392\u9664\u7684\u5b9e\u4f53" + }, + "include": { + "data": { + "entities": "\u5b9e\u4f53" + }, + "description": "\u9664\u975e\u5df2\u9009\u62e9\u4e86\u6307\u5b9a\u7684\u5b9e\u4f53\uff0c\u5426\u5219\u6240\u6709\u201c{domains}\u201d\u7c7b\u578b\u7684\u5b9e\u4f53\u90fd\u5c06\u88ab\u5305\u542b\u3002", + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" + }, "include_exclude": { "data": { "entities": "\u5b9e\u4f53", "mode": "\u6a21\u5f0f" }, - "description": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53\u3002\u5728\u9644\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u5f00\u653e\u4e00\u4e2a\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u5305\u542b\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u5305\u542b\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u90fd\u4f1a\u5f00\u653e\u3002\u5728\u6865\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u6392\u9664\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u4e5f\u90fd\u4f1a\u5f00\u653e\u3002", + "description": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53\u3002\n\u5728\u914d\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u5f00\u653e\u4e00\u4e2a\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u5305\u542b\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u5305\u542b\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u90fd\u4f1a\u5f00\u653e\u3002\u5728\u6865\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u6392\u9664\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u4e5f\u90fd\u4f1a\u5f00\u653e\u3002\n\u4e3a\u83b7\u5f97\u6700\u4f73\u4f53\u9a8c\uff0c\u5c06\u4f1a\u4e3a\u6bcf\u4e2a\u7535\u89c6\u5a92\u4f53\u64ad\u653e\u5668\u3001\u57fa\u4e8e\u6d3b\u52a8\u7684\u9065\u63a7\u5668\u3001\u9501\u548c\u6444\u50cf\u5934\u521b\u5efa\u5355\u72ec\u7684 HomeKit \u914d\u4ef6\u3002", "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" }, "init": { "data": { + "domains": "\u8981\u5305\u542b\u7684\u57df", "include_domains": "\u8981\u5305\u542b\u7684\u57df", - "mode": "\u6a21\u5f0f" + "include_exclude_mode": "\u5305\u542b\u6a21\u5f0f", + "mode": "HomeKit \u6a21\u5f0f" }, "description": "HomeKit \u53ef\u4ee5\u88ab\u914d\u7f6e\u4e3a\u5bf9\u5916\u5c55\u793a\u4e00\u4e2a\u6865\u63a5\u5668\u6216\u5355\u4e2a\u914d\u4ef6\u3002\u5728\u914d\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u4f7f\u7528\u4e00\u4e2a\u5b9e\u4f53\u3002\u8bbe\u5907\u7c7b\u578b\u4e3a\u201c\u7535\u89c6\u201d\u7684\u5a92\u4f53\u64ad\u653e\u5668\u5fc5\u987b\u4f7f\u7528\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u5de5\u4f5c\u3002\u201c\u8981\u5305\u542b\u7684\u57df\u201d\u4e2d\u7684\u5b9e\u4f53\u5c06\u5411 HomeKit \u5f00\u653e\u3002\u5728\u4e0b\u4e00\u9875\u53ef\u4ee5\u9009\u62e9\u8981\u5305\u542b\u6216\u6392\u9664\u5176\u4e2d\u7684\u54ea\u4e9b\u5b9e\u4f53\u3002", "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u57df\u3002" diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 54d6424e8d5dc..82d254cb9a53e 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -39,6 +39,7 @@ CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, + CHAR_TARGET_FAN_STATE, PROP_MIN_STEP, SERV_FANV2, SERV_SWITCH, @@ -58,35 +59,38 @@ class Fan(HomeAccessory): def __init__(self, *args): """Initialize a new Fan accessory object.""" super().__init__(*args, category=CATEGORY_FAN) - chars = [] + self.chars = [] state = self.hass.states.get(self.entity_id) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) percentage_step = state.attributes.get(ATTR_PERCENTAGE_STEP, 1) - preset_modes = state.attributes.get(ATTR_PRESET_MODES) + self.preset_modes = state.attributes.get(ATTR_PRESET_MODES) if features & SUPPORT_DIRECTION: - chars.append(CHAR_ROTATION_DIRECTION) + self.chars.append(CHAR_ROTATION_DIRECTION) if features & SUPPORT_OSCILLATE: - chars.append(CHAR_SWING_MODE) + self.chars.append(CHAR_SWING_MODE) if features & SUPPORT_SET_SPEED: - chars.append(CHAR_ROTATION_SPEED) + self.chars.append(CHAR_ROTATION_SPEED) + if self.preset_modes and len(self.preset_modes) == 1: + self.chars.append(CHAR_TARGET_FAN_STATE) - serv_fan = self.add_preload_service(SERV_FANV2, chars) + serv_fan = self.add_preload_service(SERV_FANV2, self.chars) self.set_primary_service(serv_fan) self.char_active = serv_fan.configure_char(CHAR_ACTIVE, value=0) self.char_direction = None self.char_speed = None self.char_swing = None + self.char_target_fan_state = None self.preset_mode_chars = {} - if CHAR_ROTATION_DIRECTION in chars: + if CHAR_ROTATION_DIRECTION in self.chars: self.char_direction = serv_fan.configure_char( CHAR_ROTATION_DIRECTION, value=0 ) - if CHAR_ROTATION_SPEED in chars: + if CHAR_ROTATION_SPEED in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. @@ -96,8 +100,13 @@ def __init__(self, *args): properties={PROP_MIN_STEP: percentage_step}, ) - if preset_modes: - for preset_mode in preset_modes: + if self.preset_modes and len(self.preset_modes) == 1: + self.char_target_fan_state = serv_fan.configure_char( + CHAR_TARGET_FAN_STATE, + value=0, + ) + elif self.preset_modes: + for preset_mode in self.preset_modes: preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME) serv_fan.add_linked_service(preset_serv) preset_serv.configure_char( @@ -115,7 +124,7 @@ def __init__(self, *args): ), ) - if CHAR_SWING_MODE in chars: + if CHAR_SWING_MODE in self.chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) self.async_update_state(state) serv_fan.setter_callback = self._set_chars @@ -148,6 +157,24 @@ def _set_chars(self, char_values): # get the speed they asked for if CHAR_ROTATION_SPEED in char_values: self.set_percentage(char_values[CHAR_ROTATION_SPEED]) + if CHAR_TARGET_FAN_STATE in char_values: + self.set_single_preset_mode(char_values[CHAR_TARGET_FAN_STATE]) + + def set_single_preset_mode(self, value): + """Set auto call came from HomeKit.""" + params = {ATTR_ENTITY_ID: self.entity_id} + if value: + _LOGGER.debug( + "%s: Set auto to 1 (%s)", self.entity_id, self.preset_modes[0] + ) + params[ATTR_PRESET_MODE] = self.preset_modes[0] + self.async_call_service(DOMAIN, SERVICE_SET_PRESET_MODE, params) + else: + current_state = self.hass.states.get(self.entity_id) + percentage = current_state.attributes.get(ATTR_PERCENTAGE) or 50 + params[ATTR_PERCENTAGE] = percentage + _LOGGER.debug("%s: Set auto to 0", self.entity_id) + self.async_call_service(DOMAIN, SERVICE_TURN_ON, params) def set_preset_mode(self, value, preset_mode): """Set preset_mode if call came from HomeKit.""" @@ -193,6 +220,7 @@ def async_update_state(self, new_state): """Update fan after state change.""" # Handle State state = new_state.state + attributes = new_state.attributes if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 self.char_active.set_value(self._state) @@ -208,7 +236,7 @@ def async_update_state(self, new_state): if self.char_speed is not None and state != STATE_OFF: # We do not change the homekit speed when turning off # as it will clear the restore state - percentage = new_state.attributes.get(ATTR_PERCENTAGE) + percentage = attributes.get(ATTR_PERCENTAGE) # If the homeassistant component reports its speed as the first entry # in its speed list but is not off, the hk_speed_value is 0. But 0 # is a special value in homekit. When you turn on a homekit accessory @@ -227,12 +255,18 @@ def async_update_state(self, new_state): # Handle Oscillating if self.char_swing is not None: - oscillating = new_state.attributes.get(ATTR_OSCILLATING) + oscillating = attributes.get(ATTR_OSCILLATING) if isinstance(oscillating, bool): hk_oscillating = 1 if oscillating else 0 self.char_swing.set_value(hk_oscillating) - current_preset_mode = new_state.attributes.get(ATTR_PRESET_MODE) + current_preset_mode = attributes.get(ATTR_PRESET_MODE) + if self.char_target_fan_state is not None: + # Handle single preset mode + self.char_target_fan_state.set_value(int(current_preset_mode is not None)) + return + + # Handle multiple preset modes for preset_mode, char in self.preset_mode_chars.items(): hk_value = 1 if preset_mode == current_preset_mode else 0 char.set_value(hk_value) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 5f925e2b01dc4..8c54896e85e48 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -6,6 +6,8 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, @@ -13,6 +15,8 @@ ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, + ATTR_SWING_MODE, + ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, @@ -25,6 +29,13 @@ DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -32,12 +43,21 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ) from homeassistant.components.water_heater import ( DOMAIN as DOMAIN_WATER_HEATER, @@ -51,15 +71,24 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback +from homeassistant.core import State, callback +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .accessories import TYPES, HomeAccessory from .const import ( + CHAR_ACTIVE, CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_CURRENT_FAN_STATE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, + CHAR_ROTATION_SPEED, + CHAR_SWING_MODE, + CHAR_TARGET_FAN_STATE, CHAR_TARGET_HEATING_COOLING, CHAR_TARGET_HUMIDITY, CHAR_TARGET_TEMPERATURE, @@ -67,7 +96,9 @@ DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, + PROP_MIN_STEP, PROP_MIN_VALUE, + SERV_FANV2, SERV_THERMOSTAT, ) from .util import temperature_to_homekit, temperature_to_states @@ -103,6 +134,11 @@ HC_HEAT_COOL_OFF, ] +ORDERED_FAN_SPEEDS = [FAN_LOW, FAN_MIDDLE, FAN_MEDIUM, FAN_HIGH] +PRE_DEFINED_FAN_MODES = set(ORDERED_FAN_SPEEDS) +SWING_MODE_PREFERRED_ORDER = [SWING_ON, SWING_BOTH, SWING_HORIZONTAL, SWING_VERTICAL] +PRE_DEFINED_SWING_MODES = set(SWING_MODE_PREFERRED_ORDER) + HC_MIN_TEMP = 10 HC_MAX_TEMP = 38 @@ -127,6 +163,19 @@ CURRENT_HVAC_FAN: HC_HEAT_COOL_COOL, } +FAN_STATE_INACTIVE = 0 +FAN_STATE_IDLE = 1 +FAN_STATE_ACTIVE = 2 + +HC_HASS_TO_HOMEKIT_FAN_STATE = { + CURRENT_HVAC_OFF: FAN_STATE_INACTIVE, + CURRENT_HVAC_IDLE: FAN_STATE_IDLE, + CURRENT_HVAC_HEAT: FAN_STATE_ACTIVE, + CURRENT_HVAC_COOL: FAN_STATE_ACTIVE, + CURRENT_HVAC_DRY: FAN_STATE_ACTIVE, + CURRENT_HVAC_FAN: FAN_STATE_ACTIVE, +} + HEAT_COOL_DEADBAND = 5 @@ -144,9 +193,11 @@ def __init__(self, *args): # Add additional characteristics if auto mode is supported self.chars = [] - state = self.hass.states.get(self.entity_id) - min_humidity = state.attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY) - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self.fan_chars = [] + state: State = self.hass.states.get(self.entity_id) + attributes = state.attributes + min_humidity = attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY) + features = attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( @@ -157,6 +208,7 @@ def __init__(self, *args): self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY)) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) + self.set_primary_service(serv_thermostat) # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( @@ -233,10 +285,116 @@ def __init__(self, *args): CHAR_CURRENT_HUMIDITY, value=50 ) + fan_modes = self.fan_modes = { + fan_mode.lower(): fan_mode + for fan_mode in attributes.get(ATTR_FAN_MODES, []) + } + self.ordered_fan_speeds = [] + if ( + features & SUPPORT_FAN_MODE + and fan_modes + and PRE_DEFINED_FAN_MODES.intersection(fan_modes) + ): + self.ordered_fan_speeds = [ + speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes + ] + self.fan_chars.append(CHAR_ROTATION_SPEED) + + if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds): + self.fan_chars.append(CHAR_TARGET_FAN_STATE) + + self.fan_modes = fan_modes + if ( + features & SUPPORT_SWING_MODE + and (swing_modes := attributes.get(ATTR_SWING_MODES)) + and PRE_DEFINED_SWING_MODES.intersection(swing_modes) + ): + self.swing_on_mode = next( + iter( + swing_mode + for swing_mode in SWING_MODE_PREFERRED_ORDER + if swing_mode in swing_modes + ) + ) + self.fan_chars.append(CHAR_SWING_MODE) + + if self.fan_chars: + if attributes.get(ATTR_HVAC_ACTION) is not None: + self.fan_chars.append(CHAR_CURRENT_FAN_STATE) + serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) + serv_thermostat.add_linked_service(serv_fan) + self.char_active = serv_fan.configure_char( + CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active + ) + if CHAR_SWING_MODE in self.fan_chars: + self.char_swing = serv_fan.configure_char( + CHAR_SWING_MODE, + value=0, + setter_callback=self._set_fan_swing_mode, + ) + self.char_swing.display_name = "Swing Mode" + if CHAR_ROTATION_SPEED in self.fan_chars: + self.char_speed = serv_fan.configure_char( + CHAR_ROTATION_SPEED, + value=100, + properties={PROP_MIN_STEP: 100 / len(self.ordered_fan_speeds)}, + setter_callback=self._set_fan_speed, + ) + self.char_speed.display_name = "Fan Mode" + if CHAR_CURRENT_FAN_STATE in self.fan_chars: + self.char_current_fan_state = serv_fan.configure_char( + CHAR_CURRENT_FAN_STATE, + value=0, + ) + self.char_current_fan_state.display_name = "Fan State" + if CHAR_TARGET_FAN_STATE in self.fan_chars and FAN_AUTO in self.fan_modes: + self.char_target_fan_state = serv_fan.configure_char( + CHAR_TARGET_FAN_STATE, + value=0, + setter_callback=self._set_fan_auto, + ) + self.char_target_fan_state.display_name = "Fan Auto" + self._async_update_state(state) serv_thermostat.setter_callback = self._set_chars + def _set_fan_swing_mode(self, swing_on) -> None: + _LOGGER.debug("%s: Set swing mode to %s", self.entity_id, swing_on) + mode = self.swing_on_mode if swing_on else SWING_OFF + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SWING_MODE: mode} + self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE, params) + + def _set_fan_speed(self, speed) -> None: + _LOGGER.debug("%s: Set fan speed to %s", self.entity_id, speed) + mode = percentage_to_ordered_list_item(self.ordered_fan_speeds, speed - 1) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} + self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + + def _get_on_mode(self) -> str: + if self.ordered_fan_speeds: + return percentage_to_ordered_list_item(self.ordered_fan_speeds, 50) + return self.fan_modes[FAN_ON] + + def _set_fan_active(self, active) -> None: + _LOGGER.debug("%s: Set fan active to %s", self.entity_id, active) + if FAN_OFF not in self.fan_modes: + _LOGGER.debug( + "%s: Fan does not support off, resetting to on", self.entity_id + ) + self.char_active.value = 1 + self.char_active.notify() + return + mode = self._get_on_mode() if active else self.fan_modes[FAN_OFF] + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} + self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + + def _set_fan_auto(self, auto) -> None: + _LOGGER.debug("%s: Set fan auto to %s", self.entity_id, auto) + mode = self.fan_modes[FAN_AUTO] if auto else self._get_on_mode() + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_FAN_MODE: mode} + self.async_call_service(DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE, params) + def _temperature_to_homekit(self, temp): return temperature_to_homekit(temp, self._unit) @@ -446,7 +604,8 @@ def async_update_state(self, new_state): @callback def _async_update_state(self, new_state): """Update state without rechecking the device features.""" - features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + attributes = new_state.attributes + features = attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Update target operation mode FIRST hvac_mode = new_state.state @@ -462,7 +621,7 @@ def _async_update_state(self, new_state): ) # Set current operation mode for supported thermostats - if hvac_action := new_state.attributes.get(ATTR_HVAC_ACTION): + if hvac_action := attributes.get(ATTR_HVAC_ACTION): homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] self.char_current_heat_cool.set_value(homekit_hvac_action) @@ -473,26 +632,26 @@ def _async_update_state(self, new_state): # Update current humidity if CHAR_CURRENT_HUMIDITY in self.chars: - current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) + current_humdity = attributes.get(ATTR_CURRENT_HUMIDITY) if isinstance(current_humdity, (int, float)): self.char_current_humidity.set_value(current_humdity) # Update target humidity if CHAR_TARGET_HUMIDITY in self.chars: - target_humdity = new_state.attributes.get(ATTR_HUMIDITY) + target_humdity = attributes.get(ATTR_HUMIDITY) if isinstance(target_humdity, (int, float)): self.char_target_humidity.set_value(target_humdity) # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: - cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) + cooling_thresh = attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(cooling_thresh, (int, float)): cooling_thresh = self._temperature_to_homekit(cooling_thresh) self.char_cooling_thresh_temp.set_value(cooling_thresh) # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: - heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) + heating_thresh = attributes.get(ATTR_TARGET_TEMP_LOW) if isinstance(heating_thresh, (int, float)): heating_thresh = self._temperature_to_homekit(heating_thresh) self.char_heating_thresh_temp.set_value(heating_thresh) @@ -504,11 +663,11 @@ def _async_update_state(self, new_state): # even if the device does not support it hc_hvac_mode = self.char_target_heat_cool.value if hc_hvac_mode == HC_HEAT_COOL_HEAT: - temp_low = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) + temp_low = attributes.get(ATTR_TARGET_TEMP_LOW) if isinstance(temp_low, (int, float)): target_temp = self._temperature_to_homekit(temp_low) elif hc_hvac_mode == HC_HEAT_COOL_COOL: - temp_high = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) + temp_high = attributes.get(ATTR_TARGET_TEMP_HIGH) if isinstance(temp_high, (int, float)): target_temp = self._temperature_to_homekit(temp_high) if target_temp: @@ -519,6 +678,44 @@ def _async_update_state(self, new_state): unit = UNIT_HASS_TO_HOMEKIT[self._unit] self.char_display_units.set_value(unit) + if self.fan_chars: + self._async_update_fan_state(new_state) + + @callback + def _async_update_fan_state(self, new_state): + """Update state without rechecking the device features.""" + attributes = new_state.attributes + + if CHAR_SWING_MODE in self.fan_chars and ( + swing_mode := attributes.get(ATTR_SWING_MODE) + ): + swing = 1 if swing_mode in PRE_DEFINED_SWING_MODES else 0 + self.char_swing.set_value(swing) + + fan_mode = attributes.get(ATTR_FAN_MODE) + fan_mode_lower = fan_mode.lower() if isinstance(fan_mode, str) else None + if ( + CHAR_ROTATION_SPEED in self.fan_chars + and fan_mode_lower in self.ordered_fan_speeds + ): + self.char_speed.set_value( + ordered_list_item_to_percentage(self.ordered_fan_speeds, fan_mode_lower) + ) + + if CHAR_TARGET_FAN_STATE in self.fan_chars: + self.char_target_fan_state.set_value(1 if fan_mode_lower == FAN_AUTO else 0) + + if CHAR_CURRENT_FAN_STATE in self.fan_chars and ( + hvac_action := attributes.get(ATTR_HVAC_ACTION) + ): + self.char_current_fan_state.set_value( + HC_HASS_TO_HOMEKIT_FAN_STATE[hvac_action] + ) + + self.char_active.set_value( + int(new_state.state != HVAC_MODE_OFF and fan_mode_lower != FAN_OFF) + ) + @TYPES.register("WaterHeater") class WaterHeater(HomeAccessory): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 3165280a37b7b..8c64b9b04432d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -424,8 +424,7 @@ def format_version(version): """Extract the version string in a format homekit can consume.""" split_ver = str(version).replace("-", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) - match = VERSION_RE.search(num_only) - if match: + if match := VERSION_RE.search(num_only): return match.group(0) return None diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 0f231fa93038c..e4f8f715bc8a2 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,33 +14,28 @@ ) from aiohomekit.model.services import Service, ServicesTypes -from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number -from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES, TRIGGERS +from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage +from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) -def escape_characteristic_name(char_name): - """Escape any dash or dots in a characteristics name.""" - return char_name.replace("-", "_").replace(".", "_") - - class HomeKitEntity(Entity): """Representation of a Home Assistant HomeKit device.""" _attr_should_poll = False - def __init__(self, accessory: HKDevice, devinfo): + def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: """Initialise a generic HomeKit device.""" self._accessory = accessory self._aid = devinfo["aid"] @@ -48,8 +43,6 @@ def __init__(self, accessory: HKDevice, devinfo): self._features = 0 self.setup() - self._signals = [] - super().__init__() @property @@ -69,9 +62,9 @@ def service(self) -> Service: """Return a Service model that this entity is attached to.""" return self.accessory.services.iid(self._iid) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Entity added to hass.""" - self._signals.append( + self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( self._accessory.signal_state_updated, self.async_write_ha_state ) @@ -80,16 +73,12 @@ async def async_added_to_hass(self): self._accessory.add_pollable_characteristics(self.pollable_characteristics) self._accessory.add_watchable_characteristics(self.watchable_characteristics) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Prepare to be removed from hass.""" self._accessory.remove_pollable_characteristics(self._aid) self._accessory.remove_watchable_characteristics(self._aid) - for signal_remove in self._signals: - signal_remove() - self._signals.clear() - - async def async_put_characteristics(self, characteristics: dict[str, Any]): + async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None: """ Write characteristics to the device. @@ -107,10 +96,10 @@ async def async_put_characteristics(self, characteristics: dict[str, Any]): payload = self.service.build_update(characteristics) return await self._accessory.put_characteristics(payload) - def setup(self): - """Configure an entity baed on its HomeKit characteristics metadata.""" - self.pollable_characteristics = [] - self.watchable_characteristics = [] + def setup(self) -> None: + """Configure an entity based on its HomeKit characteristics metadata.""" + self.pollable_characteristics: list[tuple[int, int]] = [] + self.watchable_characteristics: list[tuple[int, int]] = [] char_types = self.get_characteristic_types() @@ -124,7 +113,7 @@ def setup(self): for char in service.characteristics.filter(char_types=char_types): self._setup_characteristic(char) - def _setup_characteristic(self, char: Characteristic): + def _setup_characteristic(self, char: Characteristic) -> None: """Configure an entity based on a HomeKit characteristics metadata.""" # Build up a list of (aid, iid) tuples to poll on update() if CharacteristicPermissions.paired_read in char.perms: @@ -145,9 +134,9 @@ def unique_id(self) -> str: return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device if any.""" - return self.accessory_info.value(CharacteristicsTypes.NAME) + return self.accessory.name @property def available(self) -> bool: @@ -159,7 +148,7 @@ def device_info(self) -> DeviceInfo: """Return the device info.""" return self._accessory.device_info_for_accessory(self.accessory) - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" raise NotImplementedError @@ -182,7 +171,9 @@ class CharacteristicEntity(HomeKitEntity): the service entity. """ - def __init__(self, accessory, devinfo, char): + def __init__( + self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic + ) -> None: """Initialise a generic single characteristic HomeKit entity.""" self._char = char super().__init__(accessory, devinfo) @@ -217,14 +208,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() - async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) - hass.data[CONTROLLER] = aiohomekit.Controller( - async_zeroconf_instance=async_zeroconf_instance - ) + await async_get_controller(hass) + hass.data[KNOWN_DEVICES] = {} hass.data[TRIGGERS] = {} - async def _async_stop_homekit_controller(event): + async def _async_stop_homekit_controller(event: Event) -> None: await asyncio.gather( *( connection.async_unload() @@ -255,10 +244,10 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: # Remove cached type data from .storage/homekit_controller-entity-map hass.data[ENTITY_MAP].async_delete_map(hkid) + controller = await async_get_controller(hass) + # Remove the pairing on the device, making the device discoverable again. # Don't reuse any objects in hass.data as they are already unloaded - async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) - controller = aiohomekit.Controller(async_zeroconf_instance=async_zeroconf_instance) controller.load_pairing(hkid, dict(entry.data)) try: await controller.remove_pairing(hkid) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 25af3064d5d93..0194036db4e31 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,6 +1,10 @@ """Support for Homekit Alarm Control Panel.""" +from __future__ import annotations + +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( @@ -50,8 +54,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if service.short_type != ServicesTypes.SECURITY_SYSTEM: + def async_add_service(service: Service) -> bool: + if service.type != ServicesTypes.SECURITY_SYSTEM: return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitAlarmControlPanelEntity(conn, info)], True) @@ -63,7 +67,7 @@ def async_add_service(service): class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity): """Representation of a Homekit Alarm Control Panel.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT, @@ -72,12 +76,12 @@ def get_characteristic_types(self): ] @property - def icon(self): + def icon(self) -> str: """Return icon.""" return ICON @property - def state(self): + def state(self) -> str: """Return the state of the device.""" return CURRENT_STATE_MAP[ self.service.value(CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT) @@ -88,30 +92,30 @@ def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self.set_alarm_state(STATE_ALARM_DISARMED, code) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm command.""" await self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send stay command.""" await self.set_alarm_state(STATE_ALARM_ARMED_HOME, code) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send night command.""" await self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code) - async def set_alarm_state(self, state, code=None): + async def set_alarm_state(self, state: str, code: str | None = None) -> None: """Send state command.""" await self.async_put_characteristics( {CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: TARGET_STATE_MAP[state]} ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" battery_level = self.service.value(CharacteristicsTypes.BATTERY_LEVEL) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 3aadff93a01c5..91c1c47d47ed1 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Homekit motion sensors.""" +from __future__ import annotations + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -18,14 +20,14 @@ class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.MOTION - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.MOTION_DETECTED] @property - def is_on(self): + def is_on(self) -> bool: """Has motion been detected.""" - return self.service.value(CharacteristicsTypes.MOTION_DETECTED) + return self.service.value(CharacteristicsTypes.MOTION_DETECTED) is True class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): @@ -33,12 +35,12 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OPENING - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.CONTACT_STATE] @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on/open.""" return self.service.value(CharacteristicsTypes.CONTACT_STATE) == 1 @@ -48,12 +50,12 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.SMOKE - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.SMOKE_DETECTED] @property - def is_on(self): + def is_on(self) -> bool: """Return true if smoke is currently detected.""" return self.service.value(CharacteristicsTypes.SMOKE_DETECTED) == 1 @@ -63,12 +65,12 @@ class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.GAS - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.CARBON_MONOXIDE_DETECTED] @property - def is_on(self): + def is_on(self) -> bool: """Return true if CO is currently detected.""" return self.service.value(CharacteristicsTypes.CARBON_MONOXIDE_DETECTED) == 1 @@ -78,12 +80,12 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OCCUPANCY - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.OCCUPANCY_DETECTED] @property - def is_on(self): + def is_on(self) -> bool: """Return true if occupancy is currently detected.""" return self.service.value(CharacteristicsTypes.OCCUPANCY_DETECTED) == 1 @@ -93,12 +95,12 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.MOISTURE - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.LEAK_DETECTED] @property - def is_on(self): + def is_on(self) -> bool: """Return true if a leak is detected from the binary sensor.""" return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1 @@ -123,8 +125,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index efb13fc34968a..7d2c737b509c3 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -19,8 +19,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity +from .connection import HKDevice @dataclass @@ -31,15 +33,15 @@ class HomeKitButtonEntityDescription(ButtonEntityDescription): BUTTON_ENTITIES: dict[str, HomeKitButtonEntityDescription] = { - CharacteristicsTypes.Vendor.HAA_SETUP: HomeKitButtonEntityDescription( - key=CharacteristicsTypes.Vendor.HAA_SETUP, + CharacteristicsTypes.VENDOR_HAA_SETUP: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.VENDOR_HAA_SETUP, name="Setup", icon="mdi:cog", entity_category=EntityCategory.CONFIG, write_value="#HAA@trcmd", ), - CharacteristicsTypes.Vendor.HAA_UPDATE: HomeKitButtonEntityDescription( - key=CharacteristicsTypes.Vendor.HAA_UPDATE, + CharacteristicsTypes.VENDOR_HAA_UPDATE: HomeKitButtonEntityDescription( + key=CharacteristicsTypes.VENDOR_HAA_UPDATE, name="Update", device_class=ButtonDeviceClass.UPDATE, entity_category=EntityCategory.CONFIG, @@ -53,15 +55,6 @@ class HomeKitButtonEntityDescription(ButtonEntityDescription): ), } -# For legacy reasons, "built-in" characteristic types are in their short form -# And vendor types don't have a short form -# This means long and short forms get mixed up in this dict, and comparisons -# don't work! -# We call get_uuid on *every* type to normalise them to the long form -# Eventually aiohomekit will use the long form exclusively amd this can be removed. -for k, v in list(BUTTON_ENTITIES.items()): - BUTTON_ENTITIES[CharacteristicsTypes.get_uuid(k)] = BUTTON_ENTITIES.pop(k) - async def async_setup_entry( hass: HomeAssistant, @@ -73,11 +66,18 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_characteristic(char: Characteristic): - if not (description := BUTTON_ENTITIES.get(char.type)): - return False + def async_add_characteristic(char: Characteristic) -> bool: + entities = [] info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([HomeKitButton(conn, info, char, description)], True) + + if description := BUTTON_ENTITIES.get(char.type): + entities.append(HomeKitButton(conn, info, char, description)) + elif entity_type := BUTTON_ENTITY_CLASSES.get(char.type): + entities.append(entity_type(conn, info, char)) + else: + return False + + async_add_entities(entities, True) return True conn.add_char_factory(async_add_characteristic) @@ -90,16 +90,16 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity): def __init__( self, - conn, - info, - char, + conn: HKDevice, + info: ConfigType, + char: Characteristic, description: HomeKitButtonEntityDescription, - ): + ) -> None: """Initialise a HomeKit button control.""" self.entity_description = description super().__init__(conn, info, char) - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [self._char.type] @@ -114,4 +114,38 @@ async def async_press(self) -> None: """Press the button.""" key = self.entity_description.key val = self.entity_description.write_value - return await self.async_put_characteristics({key: val}) + await self.async_put_characteristics({key: val}) + + +class HomeKitEcobeeClearHoldButton(CharacteristicEntity, ButtonEntity): + """Representation of a Button control for Ecobee clear hold request.""" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [] + + @property + def name(self) -> str: + """Return the name of the device if any.""" + prefix = "" + if name := super().name: + prefix = name + return f"{prefix} Clear Hold" + + async def async_press(self) -> None: + """Press the button.""" + key = self._char.type + + # If we just send true, the request doesn't always get executed by ecobee. + # Sending false value then true value will ensure that the hold gets cleared + # and schedule resumed. + # Ecobee seems to cache the state and not update it correctly, which + # causes the request to be ignored if it thinks it has no effect. + + for val in (False, True): + await self.async_put_characteristics({key: val}) + + +BUTTON_ENTITY_CLASSES: dict[str, type] = { + CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD: HomeKitEcobeeClearHoldButton, +} diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 63b06cce3381d..0ffa0a22f4d7a 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -1,6 +1,7 @@ """Support for Homekit cameras.""" from __future__ import annotations +from aiohomekit.model import Accessory from aiohomekit.model.services import ServicesTypes from homeassistant.components.camera import Camera @@ -16,7 +17,7 @@ class HomeKitCamera(AccessoryEntity, Camera): # content_type = "image/jpeg" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [] @@ -41,12 +42,12 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_accessory(accessory): + def async_add_accessory(accessory: Accessory) -> bool: stream_mgmt = accessory.services.first( service_type=ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT ) if not stream_mgmt: - return + return False info = {"aid": accessory.aid, "iid": stream_mgmt.iid} async_add_entities([HomeKitCamera(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 4cb3f24bbc45f..53aee8561e4f0 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,5 +1,8 @@ """Support for Homekit climate devices.""" +from __future__ import annotations + import logging +from typing import Any, Final from aiohomekit.model.characteristics import ( ActivationStateValues, @@ -10,10 +13,14 @@ SwingModeValues, TargetHeaterCoolerStateValues, ) -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + ClimateEntity, +) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -83,6 +90,8 @@ SWING_MODE_HASS_TO_HOMEKIT = {v: k for k, v in SWING_MODE_HOMEKIT_TO_HASS.items()} +DEFAULT_MIN_STEP: Final = 1.0 + async def async_setup_entry( hass: HomeAssistant, @@ -94,8 +103,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -107,7 +116,7 @@ def async_add_service(service): class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, @@ -119,7 +128,7 @@ def get_characteristic_types(self): CharacteristicsTypes.TEMPERATURE_CURRENT, ] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) @@ -140,7 +149,7 @@ async def async_set_temperature(self, **kwargs): hvac_mode, ) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: await self.async_put_characteristics( @@ -163,12 +172,12 @@ async def async_set_hvac_mode(self, hvac_mode): ) @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) if state == TargetHeaterCoolerStateValues.COOL: @@ -182,61 +191,75 @@ def target_temperature(self): return None @property - def target_temperature_step(self): + def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) if state == TargetHeaterCoolerStateValues.COOL and self.service.has( CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD - ].minStep + return ( + self.service[CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD].minStep + or DEFAULT_MIN_STEP + ) if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD - ].minStep - return None + return ( + self.service[CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD].minStep + or DEFAULT_MIN_STEP + ) + return DEFAULT_MIN_STEP @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum target temp.""" state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) if state == TargetHeaterCoolerStateValues.COOL and self.service.has( CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD - ].minValue + return ( + self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].minValue + or DEFAULT_MIN_TEMP + ) if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD - ].minValue + return ( + self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].minValue + or DEFAULT_MIN_TEMP + ) return super().min_temp @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum target temp.""" state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) if state == TargetHeaterCoolerStateValues.COOL and self.service.has( CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD - ].maxValue + return ( + self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].maxValue + or DEFAULT_MAX_TEMP + ) if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD ): - return self.service[ - CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD - ].maxValue + return ( + self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].maxValue + or DEFAULT_MAX_TEMP + ) return super().max_temp @property - def hvac_action(self): + def hvac_action(self) -> str | None: """Return the current running hvac operation.""" # This characteristic describes the current mode of a device, # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. @@ -250,7 +273,7 @@ def hvac_action(self): return CURRENT_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value) @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" # This characteristic describes the target mode # E.g. should the device start heating a room if the temperature @@ -262,10 +285,10 @@ def hvac_mode(self): ): return HVAC_MODE_OFF value = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) - return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value) + return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS[value] @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" valid_values = clamp_enum_to_char( TargetHeaterCoolerStateValues, @@ -278,7 +301,7 @@ def hvac_modes(self): return modes @property - def swing_mode(self): + def swing_mode(self) -> str: """Return the swing setting. Requires SUPPORT_SWING_MODE. @@ -287,7 +310,7 @@ def swing_mode(self): return SWING_MODE_HOMEKIT_TO_HASS[value] @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """Return the list of available swing modes. Requires SUPPORT_SWING_MODE. @@ -305,7 +328,7 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: ) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" features = 0 @@ -321,7 +344,7 @@ def supported_features(self): return features @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @@ -329,7 +352,7 @@ def temperature_unit(self): class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, @@ -342,12 +365,12 @@ def get_characteristic_types(self): CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET, ] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - chars = {} + chars: dict[str, Any] = {} value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - mode = MODE_HOMEKIT_TO_HASS.get(value) + mode = MODE_HOMEKIT_TO_HASS[value] if kwargs.get(ATTR_HVAC_MODE, mode) != mode: mode = kwargs[ATTR_HVAC_MODE] @@ -359,8 +382,11 @@ async def async_set_temperature(self, **kwargs): heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if (mode == HVAC_MODE_HEAT_COOL) and ( - SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + if ( + (mode == HVAC_MODE_HEAT_COOL) + and (SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features) + and heat_temp + and cool_temp ): if temp is None: temp = (cool_temp + heat_temp) / 2 @@ -376,13 +402,13 @@ async def async_set_temperature(self, **kwargs): await self.async_put_characteristics(chars) - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" await self.async_put_characteristics( {CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: humidity} ) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" await self.async_put_characteristics( { @@ -393,12 +419,12 @@ async def async_set_hvac_mode(self, hvac_mode): ) @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}) or ( @@ -409,7 +435,7 @@ def target_temperature(self): return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( @@ -421,7 +447,7 @@ def target_temperature_high(self): return None @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( @@ -433,7 +459,7 @@ def target_temperature_low(self): return None @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum target temp.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( @@ -455,7 +481,7 @@ def min_temp(self): return super().min_temp @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum target temp.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( @@ -477,37 +503,37 @@ def max_temp(self): return super().max_temp @property - def current_humidity(self): + def current_humidity(self) -> int: """Return the current humidity.""" return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) @property - def target_humidity(self): + def target_humidity(self) -> int: """Return the humidity we try to reach.""" return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET) @property - def min_humidity(self): + def min_humidity(self) -> int: """Return the minimum humidity.""" min_humidity = self.service[ CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET ].minValue if min_humidity is not None: - return min_humidity + return int(min_humidity) return super().min_humidity @property - def max_humidity(self): + def max_humidity(self) -> int: """Return the maximum humidity.""" max_humidity = self.service[ CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET ].maxValue if max_humidity is not None: - return max_humidity + return int(max_humidity) return super().max_humidity @property - def hvac_action(self): + def hvac_action(self) -> str | None: """Return the current running hvac operation.""" # This characteristic describes the current mode of a device, # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. @@ -516,17 +542,17 @@ def hvac_action(self): return CURRENT_MODE_HOMEKIT_TO_HASS.get(value) @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" # This characteristic describes the target mode # E.g. should the device start heating a room if the temperature # falls below the target temperature. # Can be 0 - 3 (Off, Heat, Cool, Auto) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - return MODE_HOMEKIT_TO_HASS.get(value) + return MODE_HOMEKIT_TO_HASS[value] @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" valid_values = clamp_enum_to_char( HeatingCoolingTargetValues, @@ -535,7 +561,7 @@ def hvac_modes(self): return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" features = 0 @@ -553,7 +579,7 @@ def supported_features(self): return features @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 26055b964f8de..d41eb0ed2201d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,9 +1,13 @@ """Config flow to configure homekit_controller.""" +from __future__ import annotations + import logging import re +from typing import Any import aiohomekit from aiohomekit.exceptions import AuthenticationError +from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes import voluptuous as vol from homeassistant import config_entries @@ -15,8 +19,8 @@ async_get_registry as async_get_device_registry, ) -from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES +from .utils import async_get_controller HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -32,8 +36,6 @@ PAIRING_FILE = "pairing.json" -MDNS_SUFFIX = "._hap._tcp.local." - PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) @@ -55,20 +57,21 @@ } -def normalize_hkid(hkid): +def normalize_hkid(hkid: str) -> str: """Normalize a hkid so that it is safe to compare with other normalized hkids.""" return hkid.lower() @callback -def find_existing_host(hass, serial): +def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("AccessoryPairingID") == serial: return entry + return None -def ensure_pin_format(pin, allow_insecure_setup_codes=None): +def ensure_pin_format(pin: str, allow_insecure_setup_codes: Any = None) -> str: """ Ensure a pin code is correctly formatted. @@ -100,10 +103,7 @@ def __init__(self): async def _async_setup_controller(self): """Create the controller.""" - async_zeroconf_instance = await zeroconf.async_get_async_instance(self.hass) - self.controller = aiohomekit.Controller( - async_zeroconf_instance=async_zeroconf_instance - ) + self.controller = await async_get_controller(self.hass) async def async_step_user(self, user_input=None): """Handle a flow start.""" @@ -111,9 +111,10 @@ async def async_step_user(self, user_input=None): if user_input is not None: key = user_input["device"] - self.hkid = self.devices[key].device_id - self.model = self.devices[key].info["md"] - self.name = key[: -len(MDNS_SUFFIX)] if key.endswith(MDNS_SUFFIX) else key + self.hkid = self.devices[key].description.id + self.model = self.devices[key].description.model + self.name = self.devices[key].description.name + await self.async_set_unique_id( normalize_hkid(self.hkid), raise_on_progress=False ) @@ -123,15 +124,12 @@ async def async_step_user(self, user_input=None): if self.controller is None: await self._async_setup_controller() - all_hosts = await self.controller.discover_ip() - self.devices = {} - for host in all_hosts: - status_flags = int(host.info["sf"]) - paired = not status_flags & 0x01 - if paired: + + async for discovery in self.controller.async_discover(): + if discovery.paired: continue - self.devices[host.info["name"]] = host + self.devices[discovery.description.name] = discovery if not self.devices: return self.async_abort(reason="no_devices") @@ -152,33 +150,16 @@ async def async_step_unignore(self, user_input): if self.controller is None: await self._async_setup_controller() - devices = await self.controller.discover_ip(max_seconds=5) - for device in devices: - if normalize_hkid(device.device_id) != unique_id: - continue - record = device.info - return await self.async_step_zeroconf( - zeroconf.ZeroconfServiceInfo( - host=record["address"], - port=record["port"], - hostname=record["name"], - type="_hap._tcp.local.", - name=record["name"], - properties={ - "md": record["md"], - "pv": record["pv"], - zeroconf.ATTR_PROPERTIES_ID: unique_id, - "c#": record["c#"], - "s#": record["s#"], - "ff": record["ff"], - "ci": record["ci"], - "sf": record["sf"], - "sh": "", - }, - ) - ) + try: + device = await self.controller.async_find(unique_id) + except aiohomekit.AccessoryNotFoundError: + return self.async_abort(reason="accessory_not_found_error") - return self.async_abort(reason="no_devices") + self.name = device.description.name + self.model = device.description.model + self.hkid = device.description.id + + return self._async_step_pair_show_form() async def _hkid_is_homekit(self, hkid): """Determine if the device is a homekit bridge or accessory.""" @@ -280,9 +261,13 @@ async def async_step_zeroconf( if self.controller is None: await self._async_setup_controller() + # mypy can't see that self._async_setup_controller() always sets self.controller or throws + assert self.controller + pairing = self.controller.load_pairing( existing.data["AccessoryPairingID"], dict(existing.data) ) + try: await pairing.list_accessories_and_characteristics() except AuthenticationError: @@ -350,12 +335,12 @@ async def async_step_pair(self, pair_info=None): # If it doesn't have a screen then the pin is static. # If it has a display it will display a pin on that display. In - # this case the code is random. So we have to call the start_pairing + # this case the code is random. So we have to call the async_start_pairing # API before the user can enter a pin. But equally we don't want to - # call start_pairing when the device is discovered, only when they + # call async_start_pairing when the device is discovered, only when they # click on 'Configure' in the UI. - # start_pairing will make the device show its pin and return a + # async_start_pairing will make the device show its pin and return a # callable. We call the callable with the pin that the user has typed # in. @@ -410,8 +395,8 @@ async def async_step_pair(self, pair_info=None): # we always check to see if self.finish_paring has been # set. try: - discovery = await self.controller.find_ip_by_device_id(self.hkid) - self.finish_pairing = await discovery.start_pairing(self.hkid) + discovery = await self.controller.async_find(self.hkid) + self.finish_pairing = await discovery.async_start_pairing(self.hkid) except aiohomekit.BusyError: # Already performing a pair setup operation with a different @@ -489,8 +474,11 @@ async def _entry_from_accessory(self, pairing): if not (accessories := pairing_data.pop("accessories", None)): accessories = await pairing.list_accessories_and_characteristics() - bridge_info = get_bridge_information(accessories) - name = get_accessory_name(bridge_info) + parsed = Accessories.from_list(accessories) + accessory_info = parsed.aid(1).services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + name = accessory_info.value(CharacteristicsTypes.NAME, "") 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 1e011eaf15dfd..9642d5d3bc59d 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -1,7 +1,11 @@ """Helpers for managing a pairing with a HomeKit accessory or bridge.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable import datetime import logging +from typing import Any from aiohomekit.exceptions import ( AccessoryDisconnectedError, @@ -9,11 +13,11 @@ EncryptionError, ) from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.characteristics import Characteristic +from aiohomekit.model.services import Service from homeassistant.const import ATTR_VIA_DEVICE -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -37,8 +41,12 @@ _LOGGER = logging.getLogger(__name__) +AddAccessoryCb = Callable[[Accessory], bool] +AddServiceCb = Callable[[Service], bool] +AddCharacteristicCb = Callable[[Characteristic], bool] + -def valid_serial_number(serial): +def valid_serial_number(serial: str) -> bool: """Return if the serial number appears to be valid.""" if not serial: return False @@ -48,40 +56,10 @@ def valid_serial_number(serial): return True -def get_accessory_information(accessory): - """Obtain the accessory information service of a HomeKit device.""" - result = {} - for service in accessory["services"]: - stype = service["type"].upper() - if ServicesTypes.get_short(stype) != "accessory-information": - continue - for characteristic in service["characteristics"]: - ctype = CharacteristicsTypes.get_short(characteristic["type"]) - if "value" in characteristic: - result[ctype] = characteristic["value"] - return result - - -def get_bridge_information(accessories): - """Return the accessory info for the bridge.""" - for accessory in accessories: - if accessory["aid"] == 1: - return get_accessory_information(accessory) - return get_accessory_information(accessories[0]) - - -def get_accessory_name(accessory_info): - """Return the name field of an accessory.""" - for field in ("name", "model", "manufacturer"): - if field in accessory_info: - return accessory_info[field] - return None - - class HKDevice: """HomeKit device.""" - def __init__(self, hass, config_entry, pairing_data): + def __init__(self, hass, config_entry, pairing_data) -> None: """Initialise a generic HomeKit device.""" self.hass = hass @@ -101,28 +79,28 @@ def __init__(self, hass, config_entry, pairing_data): self.entity_map = Accessories() # A list of callbacks that turn HK accessories into entities - self.accessory_factories = [] + self.accessory_factories: list[AddAccessoryCb] = [] # A list of callbacks that turn HK service metadata into entities - self.listeners = [] + self.listeners: list[AddServiceCb] = [] # A list of callbacks that turn HK characteristics into entities - self.char_factories = [] + self.char_factories: list[AddCharacteristicCb] = [] # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just # a lightbulb. And we don't want to forward a config entry twice # (triggers a Config entry already set up error) - self.platforms = set() + self.platforms: set[str] = set() # This just tracks aid/iid pairs so we know if a HK service has been # mapped to a HA entity. - self.entities = [] + self.entities: list[tuple[int, int | None, int | None]] = [] # A map of aid -> device_id # Useful when routing events to triggers - self.devices = {} + self.devices: dict[int, str] = {} self.available = False @@ -130,13 +108,13 @@ def __init__(self, hass, config_entry, pairing_data): # Current values of all characteristics homekit_controller is tracking. # Key is a (accessory_id, characteristic_id) tuple. - self.current_state = {} + self.current_state: dict[tuple[int, int], Any] = {} - self.pollable_characteristics = [] + 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 = None + self._polling_interval_remover: CALLBACK_TYPE | None = None # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() @@ -146,33 +124,37 @@ def __init__(self, hass, config_entry, pairing_data): # This is set to True if we can't rely on serial numbers to be unique self.unreliable_serial_numbers = False - self.watchable_characteristics = [] + self.watchable_characteristics: list[tuple[int, int]] = [] self.pairing.dispatcher_connect(self.process_new_events) - def add_pollable_characteristics(self, characteristics): + def add_pollable_characteristics( + self, characteristics: list[tuple[int, int]] + ) -> None: """Add (aid, iid) pairs that we need to poll.""" self.pollable_characteristics.extend(characteristics) - def remove_pollable_characteristics(self, accessory_id): + def remove_pollable_characteristics(self, accessory_id: int) -> None: """Remove all pollable characteristics by accessory id.""" self.pollable_characteristics = [ char for char in self.pollable_characteristics if char[0] != accessory_id ] - def add_watchable_characteristics(self, characteristics): + def add_watchable_characteristics( + self, characteristics: list[tuple[int, int]] + ) -> None: """Add (aid, iid) pairs that we need to poll.""" self.watchable_characteristics.extend(characteristics) self.hass.async_create_task(self.pairing.subscribe(characteristics)) - def remove_watchable_characteristics(self, accessory_id): + def remove_watchable_characteristics(self, accessory_id: int) -> None: """Remove all pollable characteristics by accessory id.""" self.watchable_characteristics = [ char for char in self.watchable_characteristics if char[0] != accessory_id ] @callback - def async_set_available_state(self, available): + def async_set_available_state(self, available: bool) -> None: """Mark state of all entities on this connection when it becomes available or unavailable.""" _LOGGER.debug( "Called async_set_available_state with %s for %s", available, self.unique_id @@ -182,7 +164,7 @@ def async_set_available_state(self, available): self.available = available self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) - async def async_setup(self): + async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) if not cache: @@ -208,10 +190,6 @@ async def async_setup(self): def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: """Build a DeviceInfo for a given accessory.""" - info = accessory.services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION, - ) - identifiers = { ( IDENTIFIER_ACCESSORY_ID, @@ -220,16 +198,15 @@ def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: } if not self.unreliable_serial_numbers: - serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - identifiers.add((IDENTIFIER_SERIAL_NUMBER, serial_number)) + identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number)) device_info = DeviceInfo( identifiers=identifiers, - name=info.value(CharacteristicsTypes.NAME), - manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), - model=info.value(CharacteristicsTypes.MODEL, ""), - sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), - hw_version=info.value(CharacteristicsTypes.HARDWARE_REVISION, ""), + name=accessory.name, + manufacturer=accessory.manufacturer, + model=accessory.model, + sw_version=accessory.firmware_revision, + hw_version=accessory.hardware_revision, ) if accessory.aid != 1: @@ -244,7 +221,7 @@ def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: return device_info @callback - def async_migrate_devices(self): + def async_migrate_devices(self) -> None: """Migrate legacy device entries from 3-tuples to 2-tuples.""" _LOGGER.debug( "Migrating device registry entries for pairing %s", self.unique_id @@ -253,10 +230,6 @@ def async_migrate_devices(self): device_registry = dr.async_get(self.hass) for accessory in self.entity_map.accessories: - info = accessory.services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION, - ) - identifiers = { ( DOMAIN, @@ -270,13 +243,12 @@ def async_migrate_devices(self): (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, self.unique_id) ) - serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if valid_serial_number(serial_number): + if valid_serial_number(accessory.serial_number): identifiers.add( - (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, serial_number) + (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, accessory.serial_number) ) - device = device_registry.async_get_device(identifiers=identifiers) + device = device_registry.async_get_device(identifiers=identifiers) # type: ignore[arg-type] if not device: continue @@ -302,8 +274,7 @@ def async_migrate_devices(self): } if not self.unreliable_serial_numbers: - serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - new_identifiers.add((IDENTIFIER_SERIAL_NUMBER, serial_number)) + new_identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number)) else: _LOGGER.debug( "Not migrating serial number identifier for %s:aid:%s (it is wrong, not unique or unreliable)", @@ -316,7 +287,7 @@ def async_migrate_devices(self): ) @callback - def async_create_devices(self): + def async_create_devices(self) -> None: """ Build device registry entries for all accessories paired with the bridge. @@ -345,46 +316,40 @@ def async_create_devices(self): self.devices = devices @callback - def async_detect_workarounds(self): + def async_detect_workarounds(self) -> None: """Detect any workarounds that are needed for this pairing.""" unreliable_serial_numbers = False devices = set() for accessory in self.entity_map.accessories: - info = accessory.services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION, - ) - - serial_number = info.value(CharacteristicsTypes.SERIAL_NUMBER) - - if not valid_serial_number(serial_number): + if not valid_serial_number(accessory.serial_number): _LOGGER.debug( "Serial number %r is not valid, it cannot be used as a unique identifier", - serial_number, + accessory.serial_number, ) unreliable_serial_numbers = True - elif serial_number in devices: + elif accessory.serial_number in devices: _LOGGER.debug( "Serial number %r is duplicated within this pairing, it cannot be used as a unique identifier", - serial_number, + accessory.serial_number, ) unreliable_serial_numbers = True - elif serial_number == info.value(CharacteristicsTypes.HARDWARE_REVISION): + elif accessory.serial_number == accessory.hardware_revision: # This is a known bug with some devices (e.g. RYSE SmartShades) _LOGGER.debug( "Serial number %r is actually the hardware revision, it cannot be used as a unique identifier", - serial_number, + accessory.serial_number, ) unreliable_serial_numbers = True - devices.add(serial_number) + devices.add(accessory.serial_number) self.unreliable_serial_numbers = unreliable_serial_numbers - async def async_process_entity_map(self): + async def async_process_entity_map(self) -> None: """ Process the entity map and load any platforms or entities that need adding. @@ -413,23 +378,23 @@ async def async_process_entity_map(self): if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) - if not self.pairing.connection.is_connected: + if not self.pairing.is_connected: return await self.async_update() - async def async_unload(self): + 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.close() - return await self.hass.config_entries.async_unload_platforms( + await self.hass.config_entries.async_unload_platforms( self.config_entry, self.platforms ) - async def async_refresh_entity_map(self, config_num): + async def async_refresh_entity_map(self, config_num: int) -> bool: """Handle setup of a HomeKit accessory.""" try: self.accessories = await self.pairing.list_accessories_and_characteristics() @@ -449,26 +414,26 @@ async def async_refresh_entity_map(self, config_num): return True - def add_accessory_factory(self, add_entities_cb): + def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) self._add_new_entities_for_accessory([add_entities_cb]) - def _add_new_entities_for_accessory(self, handlers): + def _add_new_entities_for_accessory(self, handlers) -> None: for accessory in self.entity_map.accessories: for handler in handlers: - if (accessory.aid, None) in self.entities: + if (accessory.aid, None, None) in self.entities: continue if handler(accessory): - self.entities.append((accessory.aid, None)) + self.entities.append((accessory.aid, None, None)) break - def add_char_factory(self, add_entities_cb): + def add_char_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.char_factories.append(add_entities_cb) self._add_new_entities_for_char([add_entities_cb]) - def _add_new_entities_for_char(self, handlers): + def _add_new_entities_for_char(self, handlers) -> None: for accessory in self.entity_map.accessories: for service in accessory.services: for char in service.characteristics: @@ -479,33 +444,33 @@ def _add_new_entities_for_char(self, handlers): self.entities.append((accessory.aid, service.iid, char.iid)) break - def add_listener(self, add_entities_cb): + def add_listener(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for services.""" self.listeners.append(add_entities_cb) self._add_new_entities([add_entities_cb]) - def add_entities(self): + def add_entities(self) -> None: """Process the entity map and create HA entities.""" self._add_new_entities(self.listeners) self._add_new_entities_for_accessory(self.accessory_factories) self._add_new_entities_for_char(self.char_factories) - def _add_new_entities(self, callbacks): + def _add_new_entities(self, callbacks) -> None: for accessory in self.entity_map.accessories: aid = accessory.aid for service in accessory.services: iid = service.iid - if (aid, iid) in self.entities: + if (aid, None, iid) in self.entities: # Don't add the same entity again continue for listener in callbacks: if listener(service): - self.entities.append((aid, iid)) + self.entities.append((aid, None, iid)) break - async def async_load_platform(self, platform): + async def async_load_platform(self, platform: str) -> None: """Load a single platform idempotently.""" if platform in self.platforms: return @@ -519,20 +484,19 @@ async def async_load_platform(self, platform): self.platforms.remove(platform) raise - async def async_load_platforms(self): + async def async_load_platforms(self) -> None: """Load any platforms needed by this HomeKit device.""" tasks = [] - for accessory in self.accessories: - for service in accessory["services"]: - stype = ServicesTypes.get_short(service["type"].upper()) - if stype in HOMEKIT_ACCESSORY_DISPATCH: - platform = HOMEKIT_ACCESSORY_DISPATCH[stype] + for accessory in self.entity_map.accessories: + for service in accessory.services: + if service.type in HOMEKIT_ACCESSORY_DISPATCH: + platform = HOMEKIT_ACCESSORY_DISPATCH[service.type] if platform not in self.platforms: tasks.append(self.async_load_platform(platform)) - for char in service["characteristics"]: - if char["type"].upper() in CHARACTERISTIC_PLATFORMS: - platform = CHARACTERISTIC_PLATFORMS[char["type"].upper()] + for char in service.characteristics: + if char.type in CHARACTERISTIC_PLATFORMS: + platform = CHARACTERISTIC_PLATFORMS[char.type] if platform not in self.platforms: tasks.append(self.async_load_platform(platform)) @@ -542,7 +506,7 @@ async def async_load_platforms(self): async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - self.async_set_available_state(self.pairing.connection.is_connected) + self.async_set_available_state(self.pairing.is_connected) _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id ) @@ -589,7 +553,7 @@ async def async_update(self, now=None): _LOGGER.debug("Finished HomeKit controller update: %s", self.unique_id) - def process_new_events(self, new_values_dict): + def process_new_events(self, new_values_dict) -> None: """Process events from accessory into HA state.""" self.async_set_available_state(True) @@ -606,11 +570,11 @@ def process_new_events(self, new_values_dict): self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) - async def get_characteristics(self, *args, **kwargs): + async def get_characteristics(self, *args, **kwargs) -> dict[str, Any]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) - async def put_characteristics(self, characteristics): + async def put_characteristics(self, characteristics) -> None: """Control a HomeKit device state from Home Assistant.""" results = await self.pairing.put_characteristics(characteristics) @@ -635,20 +599,10 @@ async def put_characteristics(self, characteristics): self.process_new_events(new_entity_state) @property - def unique_id(self): + def unique_id(self) -> str: """ Return a unique id for this accessory or bridge. This id is random and will change if a device undergoes a hard reset. """ return self.pairing_data["AccessoryPairingID"] - - @property - def connection_info(self): - """Return accessory information for the main accessory.""" - return get_bridge_information(self.accessories) - - @property - def name(self): - """Name of the bridge accessory.""" - return get_accessory_name(self.connection_info) or self.unique_id diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index d0dfa9bad4f01..c27527d363873 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,5 +1,8 @@ """Constants for the homekit_controller component.""" +from typing import Final + from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes DOMAIN = "homekit_controller" @@ -18,53 +21,63 @@ # Mapping from Homekit type to component. HOMEKIT_ACCESSORY_DISPATCH = { - "lightbulb": "light", - "outlet": "switch", - "switch": "switch", - "thermostat": "climate", - "heater-cooler": "climate", - "security-system": "alarm_control_panel", - "garage-door-opener": "cover", - "window": "cover", - "window-covering": "cover", - "lock-mechanism": "lock", - "contact": "binary_sensor", - "motion": "binary_sensor", - "carbon-dioxide": "sensor", - "humidity": "sensor", - "humidifier-dehumidifier": "humidifier", - "light": "sensor", - "temperature": "sensor", - "battery": "sensor", - "smoke": "binary_sensor", - "carbon-monoxide": "binary_sensor", - "leak": "binary_sensor", - "fan": "fan", - "fanv2": "fan", - "occupancy": "binary_sensor", - "television": "media_player", - "valve": "switch", - "camera-rtp-stream-management": "camera", + ServicesTypes.LIGHTBULB: "light", + ServicesTypes.OUTLET: "switch", + ServicesTypes.SWITCH: "switch", + ServicesTypes.THERMOSTAT: "climate", + ServicesTypes.HEATER_COOLER: "climate", + ServicesTypes.SECURITY_SYSTEM: "alarm_control_panel", + ServicesTypes.GARAGE_DOOR_OPENER: "cover", + ServicesTypes.WINDOW: "cover", + ServicesTypes.WINDOW_COVERING: "cover", + ServicesTypes.LOCK_MECHANISM: "lock", + ServicesTypes.CONTACT_SENSOR: "binary_sensor", + ServicesTypes.MOTION_SENSOR: "binary_sensor", + ServicesTypes.CARBON_DIOXIDE_SENSOR: "sensor", + ServicesTypes.HUMIDITY_SENSOR: "sensor", + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER: "humidifier", + ServicesTypes.LIGHT_SENSOR: "sensor", + ServicesTypes.TEMPERATURE_SENSOR: "sensor", + ServicesTypes.BATTERY_SERVICE: "sensor", + ServicesTypes.SMOKE_SENSOR: "binary_sensor", + ServicesTypes.CARBON_MONOXIDE_SENSOR: "binary_sensor", + ServicesTypes.LEAK_SENSOR: "binary_sensor", + ServicesTypes.FAN: "fan", + ServicesTypes.FAN_V2: "fan", + ServicesTypes.OCCUPANCY_SENSOR: "binary_sensor", + ServicesTypes.TELEVISION: "media_player", + ServicesTypes.VALVE: "switch", + ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT: "camera", } CHARACTERISTIC_PLATFORMS = { - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: "sensor", - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: "sensor", - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: "sensor", - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR: "sensor", - CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: "number", - CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: "number", - CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: "switch", - CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: "switch", - CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: "sensor", - CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: "sensor", - CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: "number", - CharacteristicsTypes.Vendor.HAA_SETUP: "button", - CharacteristicsTypes.Vendor.HAA_UPDATE: "button", - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: "sensor", - CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", - CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY: "sensor", + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT: "sensor", + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS: "sensor", + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS_20: "sensor", + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_KW_HOUR: "sensor", + CharacteristicsTypes.VENDOR_AQARA_GATEWAY_VOLUME: "number", + CharacteristicsTypes.VENDOR_AQARA_E1_GATEWAY_VOLUME: "number", + CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: "switch", + CharacteristicsTypes.VENDOR_AQARA_E1_PAIRING_MODE: "switch", + CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_COOL: "number", + CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_HEAT: "number", + CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_COOL: "number", + CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_HEAT: "number", + CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_COOL: "number", + CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_HEAT: "number", + CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: "select", + CharacteristicsTypes.VENDOR_EVE_ENERGY_WATT: "sensor", + CharacteristicsTypes.VENDOR_EVE_DEGREE_AIR_PRESSURE: "sensor", + CharacteristicsTypes.VENDOR_EVE_DEGREE_ELEVATION: "number", + CharacteristicsTypes.VENDOR_HAA_SETUP: "button", + CharacteristicsTypes.VENDOR_HAA_UPDATE: "button", + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY: "sensor", + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY_2: "sensor", + CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: "number", + CharacteristicsTypes.VENDOR_VOCOLINC_OUTLET_ENERGY: "sensor", + CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD: "button", + CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: "number", + CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: "number", CharacteristicsTypes.TEMPERATURE_CURRENT: "sensor", CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: "sensor", CharacteristicsTypes.AIR_QUALITY: "sensor", @@ -77,12 +90,6 @@ CharacteristicsTypes.IDENTIFY: "button", } -# For legacy reasons, "built-in" characteristic types are in their short form -# And vendor types don't have a short form -# This means long and short forms get mixed up in this dict, and comparisons -# don't work! -# We call get_uuid on *every* type to normalise them to the long form -# Eventually aiohomekit will use the long form exclusively amd this can be removed. -for k, v in list(CHARACTERISTIC_PLATFORMS.items()): - value = CHARACTERISTIC_PLATFORMS.pop(k) - CHARACTERISTIC_PLATFORMS[CharacteristicsTypes.get_uuid(k)] = value + +# Device classes +DEVICE_CLASS_ECOBEE_MODE: Final = "homekit_controller__ecobee_mode" diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index ca24acc777aa2..a1aa8dbd8077e 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,10 +1,15 @@ """Support for Homekit covers.""" +from __future__ import annotations + +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASS_GARAGE, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, SUPPORT_OPEN, @@ -46,8 +51,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -59,12 +64,9 @@ def async_add_service(service): class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): """Representation of a HomeKit Garage Door.""" - @property - def device_class(self): - """Define this cover as a garage door.""" - return "garage" + _attr_device_class = DEVICE_CLASS_GARAGE - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.DOOR_STATE_CURRENT, @@ -73,47 +75,47 @@ def get_characteristic_types(self): ] @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE @property - def _state(self): + def _state(self) -> str: """Return the current state of the garage door.""" value = self.service.value(CharacteristicsTypes.DOOR_STATE_CURRENT) return CURRENT_GARAGE_STATE_MAP[value] @property - def is_closed(self): + def is_closed(self) -> bool: """Return true if cover is closed, else False.""" return self._state == STATE_CLOSED @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state == STATE_CLOSING @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state == STATE_OPENING - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Send open command.""" await self.set_door_state(STATE_OPEN) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Send close command.""" await self.set_door_state(STATE_CLOSED) - async def set_door_state(self, state): + async def set_door_state(self, state: str) -> None: """Send state command.""" await self.async_put_characteristics( {CharacteristicsTypes.DOOR_STATE_TARGET: TARGET_GARAGE_STATE_MAP[state]} ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" obstruction_detected = self.service.value( CharacteristicsTypes.OBSTRUCTION_DETECTED @@ -124,7 +126,7 @@ def extra_state_attributes(self): class HomeKitWindowCover(HomeKitEntity, CoverEntity): """Representation of a HomeKit Window or Window Covering.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.POSITION_STATE, @@ -139,7 +141,7 @@ def get_characteristic_types(self): ] @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION @@ -161,45 +163,45 @@ def supported_features(self): return features @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of cover.""" return self.service.value(CharacteristicsTypes.POSITION_CURRENT) @property - def is_closed(self): + def is_closed(self) -> bool: """Return true if cover is closed, else False.""" return self.current_cover_position == 0 @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" value = self.service.value(CharacteristicsTypes.POSITION_STATE) state = CURRENT_WINDOW_STATE_MAP[value] return state == STATE_CLOSING @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" value = self.service.value(CharacteristicsTypes.POSITION_STATE) state = CURRENT_WINDOW_STATE_MAP[value] return state == STATE_OPENING @property - def is_horizontal_tilt(self): + def is_horizontal_tilt(self) -> bool: """Return True if the service has a horizontal tilt characteristic.""" return ( self.service.value(CharacteristicsTypes.HORIZONTAL_TILT_CURRENT) is not None ) @property - def is_vertical_tilt(self): + def is_vertical_tilt(self) -> bool: """Return True if the service has a vertical tilt characteristic.""" return ( self.service.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT) is not None ) @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int: """Return current position of cover tilt.""" tilt_position = self.service.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT) if not tilt_position: @@ -208,26 +210,26 @@ def current_cover_tilt_position(self): ) return tilt_position - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Send hold command.""" await self.async_put_characteristics({CharacteristicsTypes.POSITION_HOLD: 1}) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Send open command.""" await self.async_set_cover_position(position=100) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Send close command.""" await self.async_set_cover_position(position=0) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Send position command.""" position = kwargs[ATTR_POSITION] await self.async_put_characteristics( {CharacteristicsTypes.POSITION_TARGET: position} ) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" tilt_position = kwargs[ATTR_TILT_POSITION] if self.is_vertical_tilt: @@ -240,7 +242,7 @@ async def async_set_cover_tilt_position(self, **kwargs): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" obstruction_detected = self.service.value( CharacteristicsTypes.OBSTRUCTION_DETECTED diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 5bb7d6346265f..aa2765d9be551 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for homekit devices.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import InputEventValues @@ -20,6 +20,9 @@ from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS +if TYPE_CHECKING: + from .connection import HKDevice + TRIGGER_TYPES = { "doorbell", "button1", @@ -76,7 +79,7 @@ def async_get_triggers(self): async def async_attach_trigger( self, - config: TRIGGER_SCHEMA, + config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: @@ -197,7 +200,7 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): @callback def async_add_service(service): aid = service.accessory.aid - service_type = service.short_type + service_type = service.type # If not a known service type then we can't handle any stateless events for it if service_type not in TRIGGER_FINDERS: @@ -225,7 +228,7 @@ def async_add_service(service): conn.add_listener(async_add_service) -def async_fire_triggers(conn, events): +def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): """Process events generated by a HomeKit accessory into automation triggers.""" for (aid, iid), ev in events.items(): if aid in conn.devices: diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index bdf19b6d5930f..f83ce7604cf74 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -15,7 +15,7 @@ from .const import KNOWN_DEVICES REDACTED_CHARACTERISTICS = [ - CharacteristicsTypes.get_uuid(CharacteristicsTypes.SERIAL_NUMBER), + CharacteristicsTypes.SERIAL_NUMBER, ] REDACTED_CONFIG_ENTRY_KEYS = [ @@ -44,7 +44,7 @@ async def async_get_device_diagnostics( def _async_get_diagnostics_for_device( hass: HomeAssistant, device: DeviceEntry ) -> dict[str, Any]: - data = {} + data: dict[str, Any] = {} data["name"] = device.name data["model"] = device.model @@ -60,7 +60,7 @@ def _async_get_diagnostics_for_device( include_disabled_entities=True, ) - hass_entities.sort(key=lambda entry: entry.original_name) + hass_entities.sort(key=lambda entry: entry.original_name or "") for entity_entry in hass_entities: state = hass.states.get(entity_entry.entity_id) @@ -95,7 +95,7 @@ def _async_get_diagnostics( hkid = entry.data["AccessoryPairingID"] connection: HKDevice = hass.data[KNOWN_DEVICES][hkid] - data = { + data: dict[str, Any] = { "config-entry": { "title": entry.title, "version": entry.version, @@ -112,12 +112,7 @@ def _async_get_diagnostics( for accessory in accessories: for service in accessory.get("services", []): for char in service.get("characteristics", []): - try: - normalized = CharacteristicsTypes.get_uuid(char["type"]) - except KeyError: - normalized = char["type"] - - if normalized in REDACTED_CHARACTERISTICS: + if char["type"] in REDACTED_CHARACTERISTICS: char["value"] = REDACTED if device: @@ -127,7 +122,8 @@ def _async_get_diagnostics( devices = data["devices"] = [] for device_id in connection.devices.values(): - device = device_registry.async_get(device_id) + if not (device := device_registry.async_get(device_id)): + continue devices.append(_async_get_diagnostics_for_device(hass, device)) return data diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 89b13206e90ca..71d71ce469fc2 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -1,6 +1,10 @@ """Support for Homekit fans.""" +from __future__ import annotations + +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.fan import ( DIRECTION_FORWARD, @@ -30,9 +34,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): # This must be set in subclasses to the name of a boolean characteristic # that controls whether the fan is on or off. - on_characteristic = None + on_characteristic: str - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.SWING_MODE, @@ -42,12 +46,12 @@ def get_characteristic_types(self): ] @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(self.on_characteristic) == 1 @property - def percentage(self): + def percentage(self) -> int: """Return the current speed percentage.""" if not self.is_on: return 0 @@ -55,19 +59,19 @@ def percentage(self): return self.service.value(CharacteristicsTypes.ROTATION_SPEED) @property - def current_direction(self): + def current_direction(self) -> str: """Return the current direction of the fan.""" direction = self.service.value(CharacteristicsTypes.ROTATION_DIRECTION) return HK_DIRECTION_TO_HA[direction] @property - def oscillating(self): + def oscillating(self) -> bool: """Return whether or not the fan is currently oscillating.""" oscillating = self.service.value(CharacteristicsTypes.SWING_MODE) return oscillating == 1 @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" features = 0 @@ -83,20 +87,20 @@ def supported_features(self): return features @property - def speed_count(self): + def speed_count(self) -> int: """Speed count for the fan.""" return round( min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100) / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) ) - async def async_set_direction(self, direction): + async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" await self.async_put_characteristics( {CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_HK[direction]} ) - async def async_set_percentage(self, percentage): + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan.""" if percentage == 0: return await self.async_turn_off() @@ -105,17 +109,21 @@ async def async_set_percentage(self, percentage): {CharacteristicsTypes.ROTATION_SPEED: percentage} ) - async def async_oscillate(self, oscillating: bool): + async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" await self.async_put_characteristics( {CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0} ) async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Turn the specified fan on.""" - characteristics = {} + characteristics: dict[str, Any] = {} if not self.is_on: characteristics[self.on_characteristic] = True @@ -126,7 +134,7 @@ async def async_turn_on( if characteristics: await self.async_put_characteristics(characteristics) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified fan off.""" await self.async_put_characteristics({self.on_characteristic: False}) @@ -159,8 +167,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index bde16abd23b30..fcca3e54725df 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -1,11 +1,15 @@ """Support for HomeKit Controller humidifier.""" from __future__ import annotations +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, MODE_AUTO, MODE_NORMAL, SUPPORT_MODES, @@ -37,7 +41,7 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, @@ -47,20 +51,20 @@ def get_characteristic_types(self): ] @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS | SUPPORT_MODES @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(CharacteristicsTypes.ACTIVE) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified valve on.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified valve off.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @@ -121,16 +125,22 @@ async def async_set_mode(self, mode: str) -> None: @property def min_humidity(self) -> int: """Return the minimum humidity.""" - return self.service[ - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ].minValue + return int( + self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].minValue + or DEFAULT_MIN_HUMIDITY + ) @property def max_humidity(self) -> int: """Return the maximum humidity.""" - return self.service[ - CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD - ].maxValue + return int( + self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD + ].maxValue + or DEFAULT_MAX_HUMIDITY + ) class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): @@ -138,7 +148,7 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.DEHUMIDIFIER - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, @@ -149,20 +159,20 @@ def get_characteristic_types(self): ] @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS | SUPPORT_MODES @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(CharacteristicsTypes.ACTIVE) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified valve on.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified valve off.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @@ -223,16 +233,22 @@ async def async_set_mode(self, mode: str) -> None: @property def min_humidity(self) -> int: """Return the minimum humidity.""" - return self.service[ - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ].minValue + return int( + self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].minValue + or DEFAULT_MIN_HUMIDITY + ) @property def max_humidity(self) -> int: """Return the maximum humidity.""" - return self.service[ - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD - ].maxValue + return int( + self.service[ + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + ].maxValue + or DEFAULT_MAX_HUMIDITY + ) @property def unique_id(self) -> str: @@ -251,13 +267,13 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if service.short_type != ServicesTypes.HUMIDIFIER_DEHUMIDIFIER: + def async_add_service(service: Service) -> bool: + if service.type != ServicesTypes.HUMIDIFIER_DEHUMIDIFIER: return False info = {"aid": service.accessory.aid, "iid": service.iid} - entities = [] + entities: list[HumidifierEntity] = [] if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD): entities.append(HomeKitHumidifier(conn, info)) diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 77d78074255e2..2b82f27022b24 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,6 +1,10 @@ """Support for Homekit lights.""" +from __future__ import annotations + +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -28,8 +32,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if service.short_type != ServicesTypes.LIGHTBULB: + def async_add_service(service: Service) -> bool: + if service.type != ServicesTypes.LIGHTBULB: return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitLight(conn, info)], True) @@ -41,7 +45,7 @@ def async_add_service(service): class HomeKitLight(HomeKitEntity, LightEntity): """Representation of a Homekit light.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ON, @@ -52,17 +56,17 @@ def get_characteristic_types(self): ] @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(CharacteristicsTypes.ON) @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return self.service.value(CharacteristicsTypes.BRIGHTNESS) * 255 / 100 @property - def hs_color(self): + def hs_color(self) -> tuple[float, float]: """Return the color property.""" return ( self.service.value(CharacteristicsTypes.HUE), @@ -70,12 +74,12 @@ def hs_color(self): ) @property - def color_temp(self): + def color_temp(self) -> int: """Return the color temperature.""" return self.service.value(CharacteristicsTypes.COLOR_TEMPERATURE) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" features = 0 @@ -93,7 +97,7 @@ def supported_features(self): return features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" hs_color = kwargs.get(ATTR_HS_COLOR) temperature = kwargs.get(ATTR_COLOR_TEMP) @@ -121,6 +125,6 @@ async def async_turn_on(self, **kwargs): await self.async_put_characteristics(characteristics) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" await self.async_put_characteristics({CharacteristicsTypes.ON: False}) diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index fa8601e90ff84..248bb93a68f6c 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,6 +1,10 @@ """Support for HomeKit Controller locks.""" +from __future__ import annotations + +from typing import Any + from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.lock import STATE_JAMMED, LockEntity from homeassistant.config_entries import ConfigEntry @@ -37,8 +41,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if service.short_type != ServicesTypes.LOCK_MECHANISM: + def async_add_service(service: Service) -> bool: + if service.type != ServicesTypes.LOCK_MECHANISM: return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitLock(conn, info)], True) @@ -50,7 +54,7 @@ def async_add_service(service): class HomeKitLock(HomeKitEntity, LockEntity): """Representation of a HomeKit Controller Lock.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE, @@ -59,7 +63,7 @@ def get_characteristic_types(self): ] @property - def is_locked(self): + def is_locked(self) -> bool | None: """Return true if device is locked.""" value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE) if CURRENT_STATE_MAP[value] == STATE_UNKNOWN: @@ -67,7 +71,7 @@ def is_locked(self): return CURRENT_STATE_MAP[value] == STATE_LOCKED @property - def is_locking(self): + def is_locking(self) -> bool: """Return true if device is locking.""" current_value = self.service.value( CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE @@ -81,7 +85,7 @@ def is_locking(self): ) @property - def is_unlocking(self): + def is_unlocking(self) -> bool: """Return true if device is unlocking.""" current_value = self.service.value( CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE @@ -95,27 +99,27 @@ def is_unlocking(self): ) @property - def is_jammed(self): + def is_jammed(self) -> bool: """Return true if device is jammed.""" value = self.service.value(CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE) return CURRENT_STATE_MAP[value] == STATE_JAMMED - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" await self._set_lock_state(STATE_LOCKED) - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" await self._set_lock_state(STATE_UNLOCKED) - async def _set_lock_state(self, state): + async def _set_lock_state(self, state: str) -> None: """Send state command.""" await self.async_put_characteristics( {CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: TARGET_STATE_MAP[state]} ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" attributes = {} diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7133871da42dc..dfd45991b3ffb 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,9 +3,10 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.6.11"], + "requirements": ["aiohomekit==0.7.15"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiohomekit", "commentjson"] } diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 6d87250f53c9f..6314efe9dc435 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -1,4 +1,6 @@ """Support for HomeKit Controller Televisions.""" +from __future__ import annotations + import logging from aiohomekit.model.characteristics import ( @@ -7,7 +9,7 @@ RemoteKeyValues, TargetMediaStateValues, ) -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.media_player import ( @@ -53,8 +55,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if service.short_type != ServicesTypes.TELEVISION: + def async_add_service(service: Service) -> bool: + if service.type != ServicesTypes.TELEVISION: return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([HomeKitTelevision(conn, info)], True) @@ -68,7 +70,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): _attr_device_class = MediaPlayerDeviceClass.TV - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, @@ -82,7 +84,7 @@ def get_characteristic_types(self): ] @property - def supported_features(self): + def supported_features(self) -> int: """Flag media player features that are supported.""" features = 0 @@ -108,10 +110,10 @@ def supported_features(self): return features @property - def supported_media_states(self): + def supported_media_states(self) -> set[TargetMediaStateValues]: """Mediate state flags that are supported.""" if not self.service.has(CharacteristicsTypes.TARGET_MEDIA_STATE): - return frozenset() + return set() return clamp_enum_to_char( TargetMediaStateValues, @@ -119,17 +121,17 @@ def supported_media_states(self): ) @property - def supported_remote_keys(self): + def supported_remote_keys(self) -> set[int]: """Remote key buttons that are supported.""" if not self.service.has(CharacteristicsTypes.REMOTE_KEY): - return frozenset() + return set() return clamp_enum_to_char( RemoteKeyValues, self.service[CharacteristicsTypes.REMOTE_KEY] ) @property - def source_list(self): + def source_list(self) -> list[str]: """List of all input sources for this television.""" sources = [] @@ -147,7 +149,7 @@ def source_list(self): return sources @property - def source(self): + def source(self) -> str | None: """Name of the current input source.""" active_identifier = self.service.value(CharacteristicsTypes.ACTIVE_IDENTIFIER) if not active_identifier: @@ -165,7 +167,7 @@ def source(self): return char.value @property - def state(self): + def state(self) -> str: """State of the tv.""" active = self.service.value(CharacteristicsTypes.ACTIVE) if not active: @@ -177,7 +179,7 @@ def state(self): return STATE_OK - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" if self.state == STATE_PLAYING: _LOGGER.debug("Cannot play while already playing") @@ -192,7 +194,7 @@ async def async_media_play(self): {CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE} ) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" if self.state == STATE_PAUSED: _LOGGER.debug("Cannot pause while already paused") @@ -207,7 +209,7 @@ async def async_media_pause(self): {CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE} ) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" if self.state == STATE_IDLE: _LOGGER.debug("Cannot stop when already idle") @@ -218,7 +220,7 @@ async def async_media_stop(self): {CharacteristicsTypes.TARGET_MEDIA_STATE: TargetMediaStateValues.STOP} ) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Switch to a different media source.""" this_accessory = self._accessory.entity_map.aid(self._aid) this_tv = this_accessory.services.iid(self._iid) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index c2b3dc6d7b367..b994bc80f4a21 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -9,34 +9,41 @@ from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number.const import ( + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity +from .connection import HKDevice NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { - CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( - key=CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, + CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( + key=CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, name="Spray Quantity", icon="mdi:water", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: NumberEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION, + CharacteristicsTypes.VENDOR_EVE_DEGREE_ELEVATION: NumberEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_DEGREE_ELEVATION, name="Elevation", icon="mdi:elevation-rise", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME: NumberEntityDescription( - key=CharacteristicsTypes.Vendor.AQARA_GATEWAY_VOLUME, + CharacteristicsTypes.VENDOR_AQARA_GATEWAY_VOLUME: NumberEntityDescription( + key=CharacteristicsTypes.VENDOR_AQARA_GATEWAY_VOLUME, name="Volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME: NumberEntityDescription( - key=CharacteristicsTypes.Vendor.AQARA_E1_GATEWAY_VOLUME, + CharacteristicsTypes.VENDOR_AQARA_E1_GATEWAY_VOLUME: NumberEntityDescription( + key=CharacteristicsTypes.VENDOR_AQARA_E1_GATEWAY_VOLUME, name="Volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, @@ -54,11 +61,18 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_characteristic(char: Characteristic): - if not (description := NUMBER_ENTITIES.get(char.type)): - return False + def async_add_characteristic(char: Characteristic) -> bool: + entities = [] info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([HomeKitNumber(conn, info, char, description)], True) + + if description := NUMBER_ENTITIES.get(char.type): + entities.append(HomeKitNumber(conn, info, char, description)) + elif entity_type := NUMBER_ENTITY_CLASSES.get(char.type): + entities.append(entity_type(conn, info, char)) + else: + return False + + async_add_entities(entities, True) return True conn.add_char_factory(async_add_characteristic) @@ -69,50 +83,119 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): def __init__( self, - conn, - info, - char, + conn: HKDevice, + info: ConfigType, + char: Characteristic, description: NumberEntityDescription, - ): + ) -> None: """Initialise a HomeKit number control.""" self.entity_description = description super().__init__(conn, info, char) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device if any.""" if prefix := super().name: return f"{prefix} {self.entity_description.name}" return self.entity_description.name - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [self._char.type] @property def min_value(self) -> float: """Return the minimum value.""" - return self._char.minValue + return self._char.minValue or DEFAULT_MIN_VALUE @property def max_value(self) -> float: """Return the maximum value.""" - return self._char.maxValue + return self._char.maxValue or DEFAULT_MAX_VALUE @property def step(self) -> float: """Return the increment/decrement step.""" - return self._char.minStep + return self._char.minStep or DEFAULT_STEP @property def value(self) -> float: """Return the current characteristic value.""" return self._char.value - async def async_set_value(self, value: float): + async def async_set_value(self, value: float) -> None: """Set the characteristic to this value.""" await self.async_put_characteristics( { self._char.type: value, } ) + + +class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): + """Representation of a Number control for Ecobee Fan Mode request.""" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] + + @property + def name(self) -> str: + """Return the name of the device if any.""" + prefix = "" + if name := super().name: + prefix = name + return f"{prefix} Fan Mode" + + @property + def min_value(self) -> float: + """Return the minimum value.""" + return self._char.minValue or DEFAULT_MIN_VALUE + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._char.maxValue or DEFAULT_MAX_VALUE + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + return self._char.minStep or DEFAULT_STEP + + @property + def value(self) -> float: + """Return the current characteristic value.""" + return self._char.value + + async def async_set_value(self, value: float) -> None: + """Set the characteristic to this value.""" + + # Sending the fan mode request sometimes ends up getting ignored by ecobee + # and this might be because it the older value instead of newer, and ecobee + # thinks there is nothing to do. + # So in order to make sure that the request is executed by ecobee, we need + # to send a different value before sending the target value. + # Fan mode value is a value from 0 to 100. We send a value off by 1 first. + + if value > self.min_value: + other_value = value - 1 + else: + other_value = self.min_value + 1 + + if value != other_value: + await self.async_put_characteristics( + { + self._char.type: other_value, + } + ) + + await self.async_put_characteristics( + { + self._char.type: value, + } + ) + + +NUMBER_ENTITY_CLASSES: dict[str, type] = { + CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: HomeKitEcobeeFanModeNumber, +} diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py new file mode 100644 index 0000000000000..681f24b9ab8e2 --- /dev/null +++ b/homeassistant/components/homekit_controller/select.py @@ -0,0 +1,71 @@ +"""Support for Homekit select entities.""" +from __future__ import annotations + +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import KNOWN_DEVICES, CharacteristicEntity +from .const import DEVICE_CLASS_ECOBEE_MODE + +_ECOBEE_MODE_TO_TEXT = { + 0: "home", + 1: "sleep", + 2: "away", +} +_ECOBEE_MODE_TO_NUMBERS = {v: k for (k, v) in _ECOBEE_MODE_TO_TEXT.items()} + + +class EcobeeModeSelect(CharacteristicEntity, SelectEntity): + """Represents a ecobee mode select entity.""" + + _attr_options = ["home", "sleep", "away"] + _attr_device_class = DEVICE_CLASS_ECOBEE_MODE + + @property + def name(self) -> str: + """Return the name of the device if any.""" + if name := super().name: + return f"{name} Current Mode" + return "Current Mode" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE, + ] + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return _ECOBEE_MODE_TO_TEXT.get(self._char.value) + + async def async_select_option(self, option: str) -> None: + """Set the current mode.""" + option_int = _ECOBEE_MODE_TO_NUMBERS[option] + await self.async_put_characteristics( + {CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: option_int} + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Homekit select entities.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_characteristic(char: Characteristic) -> bool: + if char.type == CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([EcobeeModeSelect(conn, info, char)]) + return True + return False + + conn.add_char_factory(async_add_characteristic) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 0cec354e1ab4a..b7d7b8005ed97 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( SensorDeviceClass, @@ -28,8 +28,10 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from .connection import HKDevice CO2_ICON = "mdi:molecule-co2" @@ -42,85 +44,85 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_WATT, + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT, name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS, + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS, name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_AMPS_20, + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS_20: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS_20, name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), - CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.CONNECTSENSE_ENERGY_KW_HOUR, + CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_KW_HOUR: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_KW_HOUR, name="Energy kWh", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, + CharacteristicsTypes.VENDOR_EVE_ENERGY_WATT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_ENERGY_WATT, name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - CharacteristicsTypes.Vendor.EVE_ENERGY_KW_HOUR: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_ENERGY_KW_HOUR, + CharacteristicsTypes.VENDOR_EVE_ENERGY_KW_HOUR: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_ENERGY_KW_HOUR, name="Energy kWh", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - CharacteristicsTypes.Vendor.EVE_ENERGY_VOLTAGE: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_ENERGY_VOLTAGE, + CharacteristicsTypes.VENDOR_EVE_ENERGY_VOLTAGE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_ENERGY_VOLTAGE, name="Volts", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), - CharacteristicsTypes.Vendor.EVE_ENERGY_AMPERE: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_ENERGY_AMPERE, + CharacteristicsTypes.VENDOR_EVE_ENERGY_AMPERE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_ENERGY_AMPERE, name="Amps", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY, + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY, name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2, + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY_2, name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), - CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE, + CharacteristicsTypes.VENDOR_EVE_DEGREE_AIR_PRESSURE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_EVE_DEGREE_AIR_PRESSURE, name="Air Pressure", device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), - CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY: HomeKitSensorEntityDescription( - key=CharacteristicsTypes.Vendor.VOCOLINC_OUTLET_ENERGY, + CharacteristicsTypes.VENDOR_VOCOLINC_OUTLET_ENERGY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.VENDOR_VOCOLINC_OUTLET_ENERGY, name="Power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -134,10 +136,7 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=TEMP_CELSIUS, # This sensor is only for temperature characteristics that are not part # of a temperature sensor service. - probe=( - lambda char: char.service.type - != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR) - ), + probe=(lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR), ), CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, @@ -147,10 +146,7 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): native_unit_of_measurement=PERCENTAGE, # This sensor is only for humidity characteristics that are not part # of a humidity sensor service. - probe=( - lambda char: char.service.type - != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR) - ), + probe=(lambda char: char.service.type != ServicesTypes.HUMIDITY_SENSOR), ), CharacteristicsTypes.AIR_QUALITY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.AIR_QUALITY, @@ -202,15 +198,6 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): ), } -# For legacy reasons, "built-in" characteristic types are in their short form -# And vendor types don't have a short form -# This means long and short forms get mixed up in this dict, and comparisons -# don't work! -# We call get_uuid on *every* type to normalise them to the long form -# Eventually aiohomekit will use the long form exclusively amd this can be removed. -for k, v in list(SIMPLE_SENSOR.items()): - SIMPLE_SENSOR[CharacteristicsTypes.get_uuid(k)] = SIMPLE_SENSOR.pop(k) - class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" @@ -218,17 +205,17 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{super().name} Humidity" @property - def native_value(self): + def native_value(self) -> float: """Return the current humidity.""" return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) @@ -239,17 +226,17 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{super().name} Temperature" @property - def native_value(self): + def native_value(self) -> float: """Return the current temperature in Celsius.""" return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) @@ -260,17 +247,17 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): _attr_device_class = SensorDeviceClass.ILLUMINANCE _attr_native_unit_of_measurement = LIGHT_LUX - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{super().name} Light Level" @property - def native_value(self): + def native_value(self) -> int: """Return the current light level in lux.""" return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) @@ -281,17 +268,17 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): _attr_icon = CO2_ICON _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{super().name} CO2" @property - def native_value(self): + def native_value(self) -> int: """Return the current CO2 level in ppm.""" return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) @@ -302,7 +289,7 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [ CharacteristicsTypes.BATTERY_LEVEL, @@ -311,12 +298,12 @@ def get_characteristic_types(self): ] @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{super().name} Battery" @property - def icon(self): + def icon(self) -> str: """Return the sensor icon.""" if not self.available or self.state is None: return "mdi:battery-unknown" @@ -338,12 +325,12 @@ def icon(self): return icon @property - def is_low_battery(self): + def is_low_battery(self) -> bool: """Return true if battery level is low.""" return self.service.value(CharacteristicsTypes.STATUS_LO_BATT) == 1 @property - def is_charging(self): + def is_charging(self) -> bool: """Return true if currently charing.""" # 0 = not charging # 1 = charging @@ -351,7 +338,7 @@ def is_charging(self): return self.service.value(CharacteristicsTypes.CHARGING_STATE) == 1 @property - def native_value(self): + def native_value(self) -> int: """Return the current battery level percentage.""" return self.service.value(CharacteristicsTypes.BATTERY_LEVEL) @@ -371,16 +358,16 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): def __init__( self, - conn, - info, - char, + conn: HKDevice, + info: ConfigType, + char: Characteristic, description: HomeKitSensorEntityDescription, - ): + ) -> None: """Initialise a secondary HomeKit characteristic sensor.""" self.entity_description = description super().__init__(conn, info, char) - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" return [self._char.type] @@ -390,7 +377,7 @@ def name(self) -> str: return f"{super().name} {self.entity_description.name}" @property - def native_value(self): + def native_value(self) -> str | int | float: """Return the current sensor value.""" return self._char.value @@ -414,8 +401,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -424,7 +411,7 @@ def async_add_service(service): conn.add_listener(async_add_service) @callback - def async_add_characteristic(char: Characteristic): + def async_add_characteristic(char: Characteristic) -> bool: if not (description := SIMPLE_SENSOR.get(char.type)): return False if description.probe and not description.probe(char): diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 4d512fbbc5dde..9372764a88af3 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -1,6 +1,10 @@ """Helpers for HomeKit data stored in HA storage.""" -from homeassistant.core import callback +from __future__ import annotations + +from typing import Any, TypedDict, cast + +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN @@ -10,6 +14,19 @@ ENTITY_MAP_SAVE_DELAY = 10 +class Pairing(TypedDict): + """A versioned map of entity metadata as presented by aiohomekit.""" + + config_num: int + accessories: list[Any] + + +class StorageLayout(TypedDict): + """Cached pairing metadata needed by aiohomekit.""" + + pairings: dict[str, Pairing] + + class EntityMapStorage: """ Holds a cache of entity structure data from a paired HomeKit device. @@ -26,34 +43,37 @@ class EntityMapStorage: very slow for these devices. """ - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Create a new entity map store.""" self.hass = hass self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY) - self.storage_data = {} + self.storage_data: dict[str, Pairing] = {} - async def async_initialize(self): + async def async_initialize(self) -> None: """Get the pairing cache data.""" if not (raw_storage := await self.store.async_load()): # There is no cached data about HomeKit devices yet return - self.storage_data = raw_storage.get("pairings", {}) + storage = cast(StorageLayout, raw_storage) + self.storage_data = storage.get("pairings", {}) - def get_map(self, homekit_id): + def get_map(self, homekit_id: str) -> Pairing | None: """Get a pairing cache item.""" return self.storage_data.get(homekit_id) @callback - def async_create_or_update_map(self, homekit_id, config_num, accessories): + def async_create_or_update_map( + self, homekit_id: str, config_num: int, accessories: list[Any] + ) -> Pairing: """Create a new pairing cache.""" - data = {"config_num": config_num, "accessories": accessories} + data = Pairing(config_num=config_num, accessories=accessories) self.storage_data[homekit_id] = data self._async_schedule_save() return data @callback - def async_delete_map(self, homekit_id): + def async_delete_map(self, homekit_id: str) -> None: """Delete pairing cache.""" if homekit_id not in self.storage_data: return @@ -62,11 +82,11 @@ def async_delete_map(self, homekit_id): self._async_schedule_save() @callback - def _async_schedule_save(self): + def _async_schedule_save(self) -> None: """Schedule saving the entity map cache.""" self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> dict[str, Any]: """Return data of entity map to store in a file.""" return {"pairings": self.storage_data} diff --git a/homeassistant/components/homekit_controller/strings.select.json b/homeassistant/components/homekit_controller/strings.select.json new file mode 100644 index 0000000000000..83f83e56ec259 --- /dev/null +++ b/homeassistant/components/homekit_controller/strings.select.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "home": "Home", + "sleep": "Sleep", + "away": "Away" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 8228d9546cb63..07d0e21e59f5b 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from aiohomekit.model.characteristics import ( Characteristic, @@ -9,15 +10,17 @@ InUseValues, IsConfiguredValues, ) -from aiohomekit.model.services import ServicesTypes +from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from .connection import HKDevice OUTLET_IN_USE = "outlet_in_use" @@ -35,14 +38,14 @@ class DeclarativeSwitchEntityDescription(SwitchEntityDescription): SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = { - CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE: DeclarativeSwitchEntityDescription( - key=CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE, + CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE, name="Pairing Mode", icon="mdi:lock-open", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE: DeclarativeSwitchEntityDescription( - key=CharacteristicsTypes.Vendor.AQARA_E1_PAIRING_MODE, + CharacteristicsTypes.VENDOR_AQARA_E1_PAIRING_MODE: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.VENDOR_AQARA_E1_PAIRING_MODE, name="Pairing Mode", icon="mdi:lock-open", entity_category=EntityCategory.CONFIG, @@ -53,35 +56,36 @@ class DeclarativeSwitchEntityDescription(SwitchEntityDescription): class HomeKitSwitch(HomeKitEntity, SwitchEntity): """Representation of a Homekit switch.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [CharacteristicsTypes.ON, CharacteristicsTypes.OUTLET_IN_USE] @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(CharacteristicsTypes.ON) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified switch on.""" await self.async_put_characteristics({CharacteristicsTypes.ON: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified switch off.""" await self.async_put_characteristics({CharacteristicsTypes.ON: False}) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes.""" outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE) if outlet_in_use is not None: return {OUTLET_IN_USE: outlet_in_use} + return None class HomeKitValve(HomeKitEntity, SwitchEntity): """Represents a valve in an irrigation system.""" - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ CharacteristicsTypes.ACTIVE, @@ -90,11 +94,11 @@ def get_characteristic_types(self): CharacteristicsTypes.REMAINING_DURATION, ] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified valve on.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified valve off.""" await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @@ -104,12 +108,12 @@ def icon(self) -> str: return "mdi:water" @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self.service.value(CharacteristicsTypes.ACTIVE) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" attrs = {} @@ -133,38 +137,38 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): def __init__( self, - conn, - info, - char, + conn: HKDevice, + info: ConfigType, + char: Characteristic, description: DeclarativeSwitchEntityDescription, - ): + ) -> None: """Initialise a HomeKit switch.""" - self.entity_description = description + self.entity_description: DeclarativeSwitchEntityDescription = description super().__init__(conn, info, char) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the device if any.""" if prefix := super().name: return f"{prefix} {self.entity_description.name}" return self.entity_description.name - def get_characteristic_types(self): + def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [self._char.type] @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._char.value == self.entity_description.true_value - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified switch on.""" await self.async_put_characteristics( {self._char.type: self.entity_description.true_value} ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified switch off.""" await self.async_put_characteristics( {self._char.type: self.entity_description.false_value} @@ -188,8 +192,8 @@ async def async_setup_entry( conn = hass.data[KNOWN_DEVICES][hkid] @callback - def async_add_service(service): - if not (entity_class := ENTITY_TYPES.get(service.short_type)): + def async_add_service(service: Service) -> bool: + if not (entity_class := ENTITY_TYPES.get(service.type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) @@ -198,7 +202,7 @@ def async_add_service(service): conn.add_listener(async_add_service) @callback - def async_add_characteristic(char: Characteristic): + def async_add_characteristic(char: Characteristic) -> bool: if not (description := SWITCH_ENTITIES.get(char.type)): return False diff --git a/homeassistant/components/homekit_controller/translations/el.json b/homeassistant/components/homekit_controller/translations/el.json index 55a2f055db0e1..ebfc1687301e3 100644 --- a/homeassistant/components/homekit_controller/translations/el.json +++ b/homeassistant/components/homekit_controller/translations/el.json @@ -1,19 +1,32 @@ { "config": { "abort": { - "invalid_properties": "\u0391\u03bd\u03b1\u03ba\u03bf\u03b9\u03bd\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + "accessory_not_found_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b6\u03b5\u03cd\u03be\u03b7\u03c2 \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af.", + "already_configured": "\u03a4\u03bf \u03b5\u03be\u03ac\u03c1\u03c4\u03b7\u03bc\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf.", + "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", + "already_paired": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03ac\u03bb\u03bb\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "ignored_model": "\u0397 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c4\u03bf\u03c5 HomeKit \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03c0\u03bb\u03bf\u03ba\u03b1\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03bc\u03b9\u03b1 \u03c0\u03b9\u03bf \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b5\u03b3\u03b3\u03b5\u03bd\u03ae\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", + "invalid_config_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03ad\u03c4\u03bf\u03b9\u03bc\u03b7 \u03b3\u03b9\u03b1 \u03b6\u03b5\u03cd\u03be\u03b7, \u03b1\u03bb\u03bb\u03ac \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03bc\u03b9\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03c1\u03bf\u03c5\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c3\u03c4\u03bf Home Assistant, \u03b7 \u03bf\u03c0\u03bf\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c0\u03c1\u03ce\u03c4\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af.", + "invalid_properties": "\u0391\u03bd\u03b1\u03ba\u03bf\u03b9\u03bd\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", + "no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c3\u03c5\u03b6\u03b5\u03c5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2" }, "error": { + "authentication_error": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 HomeKit. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "insecure_setup_code": "\u039f \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03bb\u03cc\u03b3\u03c9 \u03c4\u03b7\u03c2 \u03b1\u03c3\u03ae\u03bc\u03b1\u03bd\u03c4\u03b7\u03c2 \u03c6\u03cd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 \u03b4\u03b5\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03af \u03c4\u03b9\u03c2 \u03b2\u03b1\u03c3\u03b9\u03ba\u03ad\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2.", + "max_peers_error": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03c1\u03bd\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7 \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c7\u03ce\u03c1\u03bf \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2.", + "pairing_failed": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03af\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 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03af \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03cc\u03bd\u03c4\u03bf\u03c2.", "unable_to_pair": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "unknown_error": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03ad\u03c6\u03b5\u03c1\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1. \u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." }, + "flow_title": "{name}", "step": { "busy_error": { - "description": "\u039c\u03b1\u03c4\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03b5\u03cd\u03be\u03b7 \u03c3\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ad\u03c2 \u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7." + "description": "\u039c\u03b1\u03c4\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b6\u03b5\u03cd\u03be\u03b7 \u03c3\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ad\u03c2 \u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7.", + "title": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03ac\u03bb\u03bb\u03bf \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf" }, "max_tries_error": { - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ac\u03b2\u03b5\u03b9 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc 100 \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7." + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ac\u03b2\u03b5\u03b9 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc 100 \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7.", + "title": "\u03a5\u03c0\u03ad\u03c1\u03b2\u03b1\u03c3\u03b7 \u03c4\u03c9\u03bd \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03b9\u03ce\u03bd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "pair": { "data": { @@ -23,6 +36,10 @@ "description": "\u03a4\u03bf HomeKit Controller \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf {name} \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c7\u03c9\u03c1\u03af\u03c2 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03cc \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae HomeKit \u03ae iCloud. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2 HomeKit (\u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae XXX-XX-XXX) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1. \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c3\u03c4\u03b7\u03bd \u03af\u03b4\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03b1\u03c3\u03af\u03b1.", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 HomeKit" }, + "protocol_error": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b6\u03b5\u03cd\u03be\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c4\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03cc\u03c2 \u03c6\u03c5\u03c3\u03b9\u03ba\u03bf\u03cd \u03ae \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7.", + "title": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03be\u03ac\u03c1\u03c4\u03b7\u03bc\u03b1" + }, "user": { "data": { "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index 55f4b71f7b2ad..67b89e51b08f0 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -3,42 +3,71 @@ "abort": { "accessory_not_found_error": "N\u00e3o \u00e9 poss\u00edvel adicionar o emparelhamento, pois o dispositivo n\u00e3o pode mais ser encontrado.", "already_configured": "O acess\u00f3rio j\u00e1 est\u00e1 configurado com este controlador.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento.", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "already_paired": "Este acess\u00f3rio j\u00e1 est\u00e1 pareado com outro dispositivo. Por favor, redefina o acess\u00f3rio e tente novamente.", "ignored_model": "O suporte do HomeKit para este modelo est\u00e1 bloqueado, j\u00e1 que uma integra\u00e7\u00e3o nativa mais completa est\u00e1 dispon\u00edvel.", "invalid_config_entry": "Este dispositivo est\u00e1 mostrando como pronto para parear, mas existe um conflito na configura\u00e7\u00e3o de entrada para ele no Home Assistant que deve ser removida primeiro.", + "invalid_properties": "Propriedades inv\u00e1lidas anunciadas pelo dispositivo.", "no_devices": "N\u00e3o foi poss\u00edvel encontrar dispositivos n\u00e3o pareados" }, "error": { "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.", "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, - "flow_title": "Acess\u00f3rio HomeKit: {name}", + "flow_title": "{name}", "step": { "busy_error": { + "description": "Abortar o emparelhamento em todos os controladores, ou tentar reiniciar o dispositivo, em seguida, continuar a retomar o emparelhamento.", "title": "O dispositivo est\u00e1 pareando com outro controlador" }, "max_tries_error": { + "description": "O dispositivo recebeu mais de 100 tentativas de autentica\u00e7\u00e3o malsucedidas. Tente reiniciar o dispositivo e continue para retomar o emparelhamento.", "title": "Quantidade de tentativas de autentica\u00e7\u00e3o excedido" }, "pair": { "data": { + "allow_insecure_setup_codes": "Permitir o emparelhamento com c\u00f3digos de configura\u00e7\u00e3o inseguros.", "pairing_code": "C\u00f3digo de pareamento" }, - "description": "Digite seu c\u00f3digo de pareamento do HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio", - "title": "Parear com o acess\u00f3rio HomeKit" + "description": "O HomeKit Controller se comunica com {name} sobre a rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Digite seu c\u00f3digo de emparelhamento HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Este c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", + "title": "Emparelhar com um dispositivo atrav\u00e9s do protocolo `HomeKit Accessory`" + }, + "protocol_error": { + "description": "O dispositivo pode n\u00e3o estar no modo de emparelhamento e pode exigir um pressionamento de bot\u00e3o f\u00edsico ou virtual. Certifique-se de que o dispositivo esteja no modo de emparelhamento ou tente reinici\u00e1-lo e continue para retomar o emparelhamento.", + "title": "Erro de comunica\u00e7\u00e3o com o acess\u00f3rio" }, "user": { "data": { "device": "Dispositivo" }, - "description": "Selecione o dispositivo com o qual voc\u00ea deseja parear", - "title": "Parear com o acess\u00f3rio HomeKit" + "description": "O HomeKit Controller se comunica pela rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Selecione o dispositivo com o qual deseja emparelhar:", + "title": "Sele\u00e7\u00e3o de dispositivo" } } }, - "title": "Acess\u00f3rio HomeKit" + "device_automation": { + "trigger_subtype": { + "button1": "Bot\u00e3o 1", + "button10": "Bot\u00e3o 10", + "button2": "Bot\u00e3o 2", + "button3": "Bot\u00e3o 3", + "button4": "Bot\u00e3o 4", + "button5": "Bot\u00e3o 5", + "button6": "Bot\u00e3o 6", + "button7": "Bot\u00e3o 7", + "button8": "Bot\u00e3o 8", + "button9": "Bot\u00e3o 9", + "doorbell": "Campainha" + }, + "trigger_type": { + "double_press": "\"{subtype}\" pressionado duas vezes", + "long_press": "\"{subtype}\" pressionado e mantido", + "single_press": "\"{subtype}\" pressionado" + } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.bg.json b/homeassistant/components/homekit_controller/translations/select.bg.json new file mode 100644 index 0000000000000..5bf7e7a4d1f1e --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u041e\u0442\u0441\u044a\u0441\u0442\u0432\u0430", + "home": "\u0414\u043e\u043c", + "sleep": "\u0417\u0430\u0441\u043f\u0438\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.ca.json b/homeassistant/components/homekit_controller/translations/select.ca.json new file mode 100644 index 0000000000000..1c859fa18e329 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "A fora", + "home": "A casa", + "sleep": "Dormint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.de.json b/homeassistant/components/homekit_controller/translations/select.de.json new file mode 100644 index 0000000000000..9b0aff9789715 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Abwesend", + "home": "Zu Hause", + "sleep": "Schlafen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.el.json b/homeassistant/components/homekit_controller/translations/select.el.json new file mode 100644 index 0000000000000..3087454c361ca --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.el.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "home": "\u03a3\u03c0\u03af\u03c4\u03b9", + "sleep": "\u038e\u03c0\u03bd\u03bf\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.en.json b/homeassistant/components/homekit_controller/translations/select.en.json new file mode 100644 index 0000000000000..1468d652d21e6 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.en.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Away", + "home": "Home", + "sleep": "Sleep" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.et.json b/homeassistant/components/homekit_controller/translations/select.et.json new file mode 100644 index 0000000000000..7a81bab7b6ae5 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.et.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Eemal", + "home": "Kodus", + "sleep": "Unere\u017eiim" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.fr.json b/homeassistant/components/homekit_controller/translations/select.fr.json new file mode 100644 index 0000000000000..9da1f00799257 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Loin", + "home": "Domicile", + "sleep": "Sommeil" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.he.json b/homeassistant/components/homekit_controller/translations/select.he.json new file mode 100644 index 0000000000000..4985fb4770bb7 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.he.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u05d1\u05d7\u05d5\u05e5", + "home": "\u05d1\u05d1\u05d9\u05ea", + "sleep": "\u05e9\u05d9\u05e0\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.hu.json b/homeassistant/components/homekit_controller/translations/select.hu.json new file mode 100644 index 0000000000000..a3c80cfbccf2b --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "T\u00e1vol", + "home": "Otthon", + "sleep": "Alv\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.id.json b/homeassistant/components/homekit_controller/translations/select.id.json new file mode 100644 index 0000000000000..e279ef020b826 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Keluar", + "home": "Di Rumah", + "sleep": "Tidur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.it.json b/homeassistant/components/homekit_controller/translations/select.it.json new file mode 100644 index 0000000000000..68fc27ab80ea9 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.it.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Fuori casa", + "home": "Casa", + "sleep": "Notte" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.ja.json b/homeassistant/components/homekit_controller/translations/select.ja.json new file mode 100644 index 0000000000000..15be755add391 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u30a2\u30a6\u30a7\u30a4", + "home": "\u30db\u30fc\u30e0", + "sleep": "\u30b9\u30ea\u30fc\u30d7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.nl.json b/homeassistant/components/homekit_controller/translations/select.nl.json new file mode 100644 index 0000000000000..d330549f208f8 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.nl.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Afwezig", + "home": "Thuis", + "sleep": "Slapen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.no.json b/homeassistant/components/homekit_controller/translations/select.no.json new file mode 100644 index 0000000000000..65dabaa328afd --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.no.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Borte", + "home": "Hjemme", + "sleep": "Sove" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.pl.json b/homeassistant/components/homekit_controller/translations/select.pl.json new file mode 100644 index 0000000000000..0a59529b6ba7e --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Poza domem", + "home": "W domu", + "sleep": "U\u015bpiony" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.pt-BR.json b/homeassistant/components/homekit_controller/translations/select.pt-BR.json new file mode 100644 index 0000000000000..e807b3eacf7c7 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Fora", + "home": "Casa", + "sleep": "Dormir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.ru.json b/homeassistant/components/homekit_controller/translations/select.ru.json new file mode 100644 index 0000000000000..dd6b3fcb286f7 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u041d\u0435 \u0434\u043e\u043c\u0430", + "home": "\u0414\u043e\u043c\u0430", + "sleep": "\u0421\u043e\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.tr.json b/homeassistant/components/homekit_controller/translations/select.tr.json new file mode 100644 index 0000000000000..39fe8e6553639 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Uzakta", + "home": "Ev", + "sleep": "Uyku" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.uk.json b/homeassistant/components/homekit_controller/translations/select.uk.json new file mode 100644 index 0000000000000..b2bb2e8e3e256 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430", + "home": "\u0412\u0434\u043e\u043c\u0430", + "sleep": "\u0421\u043e\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.zh-Hans.json b/homeassistant/components/homekit_controller/translations/select.zh-Hans.json new file mode 100644 index 0000000000000..7b8bda72fff2e --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.zh-Hans.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u79bb\u5f00", + "home": "\u5728\u5bb6", + "sleep": "\u7761\u7720" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.zh-Hant.json b/homeassistant/components/homekit_controller/translations/select.zh-Hant.json new file mode 100644 index 0000000000000..f0e1588b48607 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "\u5916\u51fa", + "home": "\u5728\u5bb6", + "sleep": "\u7761\u7720" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sk.json b/homeassistant/components/homekit_controller/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index a5f57e2f57615..f0d8fdec84c63 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u5141\u8bb8\u4f7f\u7528\u4e0d\u5b89\u5168\u7684\u8bbe\u7f6e\u4ee3\u7801\u914d\u5bf9\u3002", "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, - "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3a XXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", + "description": "HomeKit \u63a7\u5236\u5668\u4f7f\u7528\u5b89\u5168\u52a0\u5bc6\u8fde\u63a5\uff0c\u901a\u8fc7\u5c40\u57df\u7f51\u4e0e\u914d\u4ef6\u76f4\u63a5\u901a\u4fe1\uff0c\u65e0\u9700\u4f7f\u7528 iCloud \u6216\u5176\u4ed6\u8f6c\u63a5\u5668\u3002\n\u8bf7\u8f93\u5165\u201c{name}\u201d\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u6b64\u4ee3\u7801\u4e3a 8 \u4f4d\u6570\u5b57\uff0c\u4ee3\u7801\u901a\u5e38\u53ef\u5728\u8bbe\u5907\u672c\u4f53\u6216\u5305\u88c5\u76d2\u4e0a\u627e\u5230\u3002", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" }, "protocol_error": { @@ -44,8 +44,8 @@ "data": { "device": "\u8bbe\u5907" }, - "description": "HomeKit \u63a7\u5236\u5668\u4f7f\u7528\u5b89\u5168\u7684\u52a0\u5bc6\u8fde\u63a5\uff0c\u901a\u8fc7\u5c40\u57df\u7f51\u76f4\u63a5\u8fdb\u884c\u901a\u4fe1\uff0c\u65e0\u9700\u5355\u72ec\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud\u3002\u8bf7\u9009\u62e9\u8981\u914d\u5bf9\u7684\u8bbe\u5907\uff1a", - "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" + "description": "HomeKit \u63a7\u5236\u5668\u4f7f\u7528\u5b89\u5168\u7684\u52a0\u5bc6\u8fde\u63a5\u901a\u8fc7\u5c40\u57df\u7f51\u4e0e\u914d\u4ef6\u76f4\u63a5\u901a\u4fe1\uff0c\u65e0\u9700\u4f7f\u7528 iCloud \u6216\u5176\u4ed6\u8f6c\u63a5\u5668\u3002\u8bf7\u9009\u62e9\u8981\u914d\u5bf9\u7684\u8bbe\u5907\uff1a", + "title": "\u9009\u62e9\u8bbe\u5907" } } }, diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py new file mode 100644 index 0000000000000..6831c3cee4add --- /dev/null +++ b/homeassistant/components/homekit_controller/utils.py @@ -0,0 +1,42 @@ +"""Helper functions for the homekit_controller component.""" +from typing import cast + +from aiohomekit import Controller + +from homeassistant.components import zeroconf +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant + +from .const import CONTROLLER + + +async def async_get_controller(hass: HomeAssistant) -> Controller: + """Get or create an aiohomekit Controller instance.""" + if existing := hass.data.get(CONTROLLER): + return cast(Controller, existing) + + async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + + # In theory another call to async_get_controller could have run while we were + # trying to get the zeroconf instance. So we check again to make sure we + # don't leak a Controller instance here. + if existing := hass.data.get(CONTROLLER): + return cast(Controller, existing) + + controller = Controller(async_zeroconf_instance=async_zeroconf_instance) + + hass.data[CONTROLLER] = controller + + async def _async_stop_homekit_controller(event: Event) -> None: + # Pop first so that in theory another controller /could/ start + # While this one was shutting down + hass.data.pop(CONTROLLER, None) + await controller.async_stop() + + # Right now _async_stop_homekit_controller is only called on HA exiting + # So we don't have to worry about leaking a callback here. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) + + await controller.async_start() + + return controller diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 6482db7ae6027..f6ba16b1c5a2d 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": ["pyhomematic==0.1.77"], "codeowners": ["@pvizeli", "@danielperna84"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyhomematic"] } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index c8dc86c334832..456a10b763020 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -272,8 +272,7 @@ def setup_platform( devices = [] for conf in discovery_info[ATTR_DISCOVER_DEVICES]: state = conf.get(ATTR_PARAM) - entity_desc = SENSOR_DESCRIPTIONS.get(state) - if entity_desc is None: + if (entity_desc := SENSOR_DESCRIPTIONS.get(state)) is None: name = conf.get(ATTR_NAME) _LOGGER.warning( "Sensor (%s) entity description is missing. Sensor state (%s) needs to be maintained", diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 1a078fa9c8d0a..b13c8ca19b21a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,6 @@ "requirements": ["homematicip==1.0.2"], "codeowners": [], "quality_scale": "platinum", - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["homematicip"] } diff --git a/homeassistant/components/homematicip_cloud/translations/el.json b/homeassistant/components/homematicip_cloud/translations/el.json new file mode 100644 index 0000000000000..dc13644ad0d0f --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/el.json @@ -0,0 +1,29 @@ +{ + "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", + "connection_aborted": "\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" + }, + "error": { + "invalid_sgtin_or_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf SGTIN \u03ae \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "press_the_button": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03c0\u03bb\u03b5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af.", + "register_failed": "\u0397 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "timeout_button": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03c0\u03af\u03b5\u03c3\u03b7\u03c2 \u03bc\u03c0\u03bb\u03b5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + }, + "step": { + "init": { + "data": { + "hapid": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (SGTIN)", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2)", + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 HomematicIP" + }, + "link": { + "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03c0\u03bb\u03b5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03bf \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf HomematicIP \u03c3\u03c4\u03bf Home Assistant.\n\n![\u0398\u03ad\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd \u03c3\u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u03a3\u03b7\u03bc\u03b5\u03af\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/pt-BR.json b/homeassistant/components/homematicip_cloud/translations/pt-BR.json index c19678ad0c494..bff773d58bd06 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt-BR.json +++ b/homeassistant/components/homematicip_cloud/translations/pt-BR.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "O Accesspoint j\u00e1 est\u00e1 configurado", - "connection_aborted": "N\u00e3o foi poss\u00edvel conectar ao servidor HMIP", - "unknown": "Ocorreu um erro desconhecido." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "connection_aborted": "Falha ao conectar", + "unknown": "Erro inesperado" }, "error": { - "invalid_sgtin_or_pin": "PIN inv\u00e1lido, por favor tente novamente.", + "invalid_sgtin_or_pin": "C\u00f3digo PIN inv\u00e1lido, por favor tente novamente.", "press_the_button": "Por favor, pressione o bot\u00e3o azul.", "register_failed": "Falha ao registrar, por favor tente novamente.", "timeout_button": "Tempo para pressionar o Bot\u00e3o Azul expirou, por favor tente novamente." @@ -16,13 +16,13 @@ "data": { "hapid": "ID do AccessPoint (SGTIN)", "name": "Nome (opcional, usado como prefixo de nome para todos os dispositivos)", - "pin": "C\u00f3digo PIN (opcional)" + "pin": "C\u00f3digo PIN" }, "title": "Escolha um HomematicIP Accesspoint" }, "link": { "description": "Pressione o bot\u00e3o azul no ponto de acesso e o bot\u00e3o enviar para registrar o HomematicIP com o Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Accesspoint link" + "title": "Link ponto de acesso" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/sk.json b/homeassistant/components/homematicip_cloud/translations/sk.json new file mode 100644 index 0000000000000..48638e0878737 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "name": "N\u00e1zov (volite\u013en\u00e9, pou\u017e\u00edva sa ako predpona pre v\u0161etky zariadenia)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 45a912fefec92..6ab485f534f8c 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -10,11 +10,18 @@ from voluptuous import Required, Schema from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import persistent_notification, zeroconf from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult -from .const import CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, DOMAIN +from .const import ( + CONF_API_ENABLED, + CONF_PATH, + CONF_PRODUCT_NAME, + CONF_PRODUCT_TYPE, + CONF_SERIAL, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -28,17 +35,18 @@ def __init__(self) -> None: """Initialize the HomeWizard config flow.""" self.config: dict[str, str | int] = {} - async def async_step_import(self, import_config: dict) -> FlowResult: + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Handle a flow initiated by older `homewizard_energy` component.""" _LOGGER.debug("config_flow async_step_import") - self.hass.components.persistent_notification.async_create( - ( + persistent_notification.async_create( + self.hass, + title="HomeWizard Energy", + message=( "The custom integration of HomeWizard Energy has been migrated to core. " "You can safely remove the custom integration from the custom_integrations folder." ), - "HomeWizard Energy", - f"homewizard_energy_to_{DOMAIN}", + notification_id=f"homewizard_energy_to_{DOMAIN}", ) return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]}) @@ -96,40 +104,33 @@ async def async_step_zeroconf( # Validate doscovery entry if ( - "api_enabled" not in discovery_info.properties - or "path" not in discovery_info.properties - or "product_name" not in discovery_info.properties - or "product_type" not in discovery_info.properties - or "serial" not in discovery_info.properties + CONF_API_ENABLED not in discovery_info.properties + or CONF_PATH not in discovery_info.properties + or CONF_PRODUCT_NAME not in discovery_info.properties + or CONF_PRODUCT_TYPE not in discovery_info.properties + or CONF_SERIAL not in discovery_info.properties ): return self.async_abort(reason="invalid_discovery_parameters") - if (discovery_info.properties["path"]) != "/api/v1": + if (discovery_info.properties[CONF_PATH]) != "/api/v1": return self.async_abort(reason="unsupported_api_version") - if (discovery_info.properties["api_enabled"]) != "1": - return self.async_abort(reason="api_not_enabled") - # Sets unique ID and aborts if it is already exists await self._async_set_and_check_unique_id( { CONF_IP_ADDRESS: discovery_info.host, - CONF_PRODUCT_TYPE: discovery_info.properties["product_type"], - CONF_SERIAL: discovery_info.properties["serial"], + CONF_PRODUCT_TYPE: discovery_info.properties[CONF_PRODUCT_TYPE], + CONF_SERIAL: discovery_info.properties[CONF_SERIAL], } ) - # Check connection and fetch - device_info: dict[str, Any] = await self._async_try_connect_and_fetch( - discovery_info.host - ) - # Pass parameters self.config = { + CONF_API_ENABLED: discovery_info.properties[CONF_API_ENABLED], CONF_IP_ADDRESS: discovery_info.host, - CONF_PRODUCT_TYPE: device_info[CONF_PRODUCT_TYPE], - CONF_PRODUCT_NAME: device_info[CONF_PRODUCT_NAME], - CONF_SERIAL: device_info[CONF_SERIAL], + CONF_PRODUCT_TYPE: discovery_info.properties[CONF_PRODUCT_TYPE], + CONF_PRODUCT_NAME: discovery_info.properties[CONF_PRODUCT_NAME], + CONF_SERIAL: discovery_info.properties[CONF_SERIAL], } return await self.async_step_discovery_confirm() @@ -138,6 +139,12 @@ async def async_step_discovery_confirm( ) -> FlowResult: """Confirm discovery.""" if user_input is not None: + if (self.config[CONF_API_ENABLED]) != "1": + raise AbortFlow(reason="api_not_enabled") + + # Check connection + await self._async_try_connect_and_fetch(str(self.config[CONF_IP_ADDRESS])) + return self.async_create_entry( title=f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})", data={ diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index 9a6c465532fe5..75c522a211e67 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -14,11 +14,13 @@ PLATFORMS = [Platform.SENSOR, Platform.SWITCH] # Platform config. -CONF_SERIAL = "serial" +CONF_API_ENABLED = "api_enabled" +CONF_DATA = "data" +CONF_DEVICE = "device" +CONF_PATH = "path" CONF_PRODUCT_NAME = "product_name" CONF_PRODUCT_TYPE = "product_type" -CONF_DEVICE = "device" -CONF_DATA = "data" +CONF_SERIAL = "serial" UPDATE_INTERVAL = timedelta(seconds=5) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 0fe2174da8347..0705da4938b72 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -9,5 +9,6 @@ ], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiohwenergy"] } diff --git a/homeassistant/components/homewizard/translations/bg.json b/homeassistant/components/homewizard/translations/bg.json index 8c2031843f43f..dedf6ca570b51 100644 --- a/homeassistant/components/homewizard/translations/bg.json +++ b/homeassistant/components/homewizard/translations/bg.json @@ -2,9 +2,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", + "device_not_supported": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430", + "invalid_discovery_parameters": "\u041e\u0442\u043a\u0440\u0438\u0442\u0430 \u0435 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 API", "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "discovery_confirm": { + "title": "\u041f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435" + }, "user": { "data": { "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/homewizard/translations/ca.json b/homeassistant/components/homewizard/translations/ca.json index c32926cf57b3a..4e855e3bc071c 100644 --- a/homeassistant/components/homewizard/translations/ca.json +++ b/homeassistant/components/homewizard/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "api_not_enabled": "L'API no est\u00e0 activada. Activa-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", "device_not_supported": "Aquest dispositiu no \u00e9s compatible", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Versi\u00f3 d'API no compatible detectada", "unknown_error": "Error inesperat" }, "step": { diff --git a/homeassistant/components/homewizard/translations/de.json b/homeassistant/components/homewizard/translations/de.json index 5a08a12d97046..782ac2bf6feaf 100644 --- a/homeassistant/components/homewizard/translations/de.json +++ b/homeassistant/components/homewizard/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "api_not_enabled": "Die API ist nicht aktiviert. Aktiviere die API in der HomeWizard Energy App unter Einstellungen", "device_not_supported": "Dieses Ger\u00e4t wird nicht unterst\u00fctzt", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Nicht unterst\u00fctzte API-Version erkannt", "unknown_error": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/homewizard/translations/el.json b/homeassistant/components/homewizard/translations/el.json index 69796cec02d2e..f3d7c392109de 100644 --- a/homeassistant/components/homewizard/translations/el.json +++ b/homeassistant/components/homewizard/translations/el.json @@ -1,9 +1,11 @@ { "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", "api_not_enabled": "\u03a4\u03bf API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf API \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae HomeWizard Energy App \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", "device_not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", - "invalid_discovery_parameters": "unsupported_api_version" + "invalid_discovery_parameters": "unsupported_api_version", + "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "discovery_confirm": { diff --git a/homeassistant/components/homewizard/translations/en.json b/homeassistant/components/homewizard/translations/en.json index 1aa1ff0a61141..b2ef3c16b1e11 100644 --- a/homeassistant/components/homewizard/translations/en.json +++ b/homeassistant/components/homewizard/translations/en.json @@ -4,7 +4,7 @@ "already_configured": "Device is already configured", "api_not_enabled": "The API is not enabled. Enable API in the HomeWizard Energy App under settings", "device_not_supported": "This device is not supported", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Detected unsupported API version", "unknown_error": "Unexpected error" }, "step": { diff --git a/homeassistant/components/homewizard/translations/et.json b/homeassistant/components/homewizard/translations/et.json index 8d8fefd6e75b2..360010b4d573b 100644 --- a/homeassistant/components/homewizard/translations/et.json +++ b/homeassistant/components/homewizard/translations/et.json @@ -4,7 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "api_not_enabled": "API pole lubatud. Luba API HomeWizard Energy rakenduse seadete all", "device_not_supported": "Seda seadet ei toetata", - "invalid_discovery_parameters": "Toetuseta API versioon", + "invalid_discovery_parameters": "Leiti toetuseta API versioon", "unknown_error": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/homewizard/translations/fr.json b/homeassistant/components/homewizard/translations/fr.json index 66d3edbc97814..6ddc51565fb84 100644 --- a/homeassistant/components/homewizard/translations/fr.json +++ b/homeassistant/components/homewizard/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "api_not_enabled": "L'API n'est pas activ\u00e9e. Activer l'API dans l'application HomeWizard Energy dans les param\u00e8tres", "device_not_supported": "Cet appareil n'est pas compatible", + "invalid_discovery_parameters": "Version d'API non prise en charge d\u00e9tect\u00e9e", "unknown_error": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/homewizard/translations/he.json b/homeassistant/components/homewizard/translations/he.json index 1ca3be234e50e..9355e0d6b807e 100644 --- a/homeassistant/components/homewizard/translations/he.json +++ b/homeassistant/components/homewizard/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "device_not_supported": "\u05d4\u05ea\u05e7\u05df \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da", "unknown_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/homewizard/translations/hu.json b/homeassistant/components/homewizard/translations/hu.json index 89386e7671780..060e7e9024895 100644 --- a/homeassistant/components/homewizard/translations/hu.json +++ b/homeassistant/components/homewizard/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "api_not_enabled": "Az API nincs enged\u00e9lyezve. Enged\u00e9lyezze az API-t a HomeWizard Energy alkalmaz\u00e1sban a be\u00e1ll\u00edt\u00e1sok k\u00f6z\u00f6tt.", "device_not_supported": "Ez az eszk\u00f6z nem t\u00e1mogatott", - "invalid_discovery_parameters": "Nem t\u00e1mogatott API verzi\u00f3", + "invalid_discovery_parameters": "Nem t\u00e1mogatott API-verzi\u00f3 \u00e9szlel\u00e9se", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/homewizard/translations/id.json b/homeassistant/components/homewizard/translations/id.json index 35ddc764aa0d6..6363e9de21d40 100644 --- a/homeassistant/components/homewizard/translations/id.json +++ b/homeassistant/components/homewizard/translations/id.json @@ -2,13 +2,22 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "api_not_enabled": "API tidak diaktifkan. Aktifkan API di Aplikasi Energi HomeWizard di bawah pengaturan", + "device_not_supported": "Perangkat ini tidak didukung", + "invalid_discovery_parameters": "Terdeteksi versi API yang tidak didukung", "unknown_error": "Kesalahan yang tidak diharapkan" }, "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {product_type} ({serial}) di {ip_address}?", + "title": "Konfirmasikan" + }, "user": { "data": { "ip_address": "Alamat IP" - } + }, + "description": "Masukkan alamat IP perangkat HomeWizard Energy Anda untuk diintegrasikan dengan Home Assistant.", + "title": "Konfigurasikan perangkat" } } } diff --git a/homeassistant/components/homewizard/translations/it.json b/homeassistant/components/homewizard/translations/it.json index c0d1be424a530..61f8a62a6c1f7 100644 --- a/homeassistant/components/homewizard/translations/it.json +++ b/homeassistant/components/homewizard/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "api_not_enabled": "L'API non \u00e8 abilitata. Abilita API nell'applicazione HomeWizard Energy sotto impostazioni", "device_not_supported": "Questo dispositivo non \u00e8 supportato", - "invalid_discovery_parameters": "versione_api_non_supportata", + "invalid_discovery_parameters": "Rilevata versione API non supportata", "unknown_error": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/homewizard/translations/lv.json b/homeassistant/components/homewizard/translations/lv.json new file mode 100644 index 0000000000000..2f9c5d4ac2029 --- /dev/null +++ b/homeassistant/components/homewizard/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "title": "Apstiprin\u0101t" + }, + "user": { + "title": "Konfigur\u0113t ier\u012bci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/nl.json b/homeassistant/components/homewizard/translations/nl.json index 0cae34cffebb0..bda434b4439ee 100644 --- a/homeassistant/components/homewizard/translations/nl.json +++ b/homeassistant/components/homewizard/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "api_not_enabled": "De API is niet ingeschakeld. Activeer API in de HomeWizard Energy App onder instellingen", "device_not_supported": "Dit apparaat wordt niet ondersteund", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Niet-ondersteunde API-versie gedetecteerd", "unknown_error": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/homewizard/translations/no.json b/homeassistant/components/homewizard/translations/no.json index b19184f1a3302..e2e0aadeb5ec5 100644 --- a/homeassistant/components/homewizard/translations/no.json +++ b/homeassistant/components/homewizard/translations/no.json @@ -4,7 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "api_not_enabled": "API-en er ikke aktivert. Aktiver API i HomeWizard Energy-appen under innstillinger", "device_not_supported": "Denne enheten st\u00f8ttes ikke", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Oppdaget API-versjon som ikke st\u00f8ttes", "unknown_error": "Uventet feil" }, "step": { diff --git a/homeassistant/components/homewizard/translations/pl.json b/homeassistant/components/homewizard/translations/pl.json index 881521a2c2995..4388fa7d5179d 100644 --- a/homeassistant/components/homewizard/translations/pl.json +++ b/homeassistant/components/homewizard/translations/pl.json @@ -2,8 +2,9 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "api_not_enabled": "Interfejs API nie jest w\u0142\u0105czony. W\u0142\u0105cz API w aplikacji HomeWizard Energy w ustawieniach.", + "api_not_enabled": "Interfejs API nie jest w\u0142\u0105czony. W\u0142\u0105cz API w ustawieniach aplikacji HomeWizard Energy.", "device_not_supported": "To urz\u0105dzenie nie jest obs\u0142ugiwane", + "invalid_discovery_parameters": "Wykryto nieobs\u0142ugiwan\u0105 wersj\u0119 API", "unknown_error": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/homewizard/translations/pt-BR.json b/homeassistant/components/homewizard/translations/pt-BR.json new file mode 100644 index 0000000000000..b1abeef992797 --- /dev/null +++ b/homeassistant/components/homewizard/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "api_not_enabled": "A API n\u00e3o est\u00e1 habilitada. Ative a API no aplicativo HomeWizard Energy em configura\u00e7\u00f5es", + "device_not_supported": "Este dispositivo n\u00e3o \u00e9 compat\u00edvel", + "invalid_discovery_parameters": "Vers\u00e3o de API n\u00e3o compat\u00edvel detectada", + "unknown_error": "Erro inesperado" + }, + "step": { + "discovery_confirm": { + "description": "Deseja configurar {product_type} ( {serial} ) em {ip_address} ?", + "title": "Confirmar" + }, + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Digite o endere\u00e7o IP de seu dispositivo HomeWizard Energy para integrar com o Home Assistant.", + "title": "Configurar dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/ru.json b/homeassistant/components/homewizard/translations/ru.json index 47d4434ed6e22..61f4cfd8feaac 100644 --- a/homeassistant/components/homewizard/translations/ru.json +++ b/homeassistant/components/homewizard/translations/ru.json @@ -4,6 +4,7 @@ "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.", "api_not_enabled": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 API \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f HomeWizard Energy.", "device_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.", + "invalid_discovery_parameters": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f API.", "unknown_error": "\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/homewizard/translations/tr.json b/homeassistant/components/homewizard/translations/tr.json index e574b74ef9195..3ed1eaf64883f 100644 --- a/homeassistant/components/homewizard/translations/tr.json +++ b/homeassistant/components/homewizard/translations/tr.json @@ -4,7 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "api_not_enabled": "API etkin de\u011fil. Ayarlar alt\u0131nda HomeWizard Energy Uygulamas\u0131nda API'yi etkinle\u015ftirin", "device_not_supported": "Bu cihaz desteklenmiyor", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Desteklenmeyen API s\u00fcr\u00fcm\u00fc alg\u0131land\u0131", "unknown_error": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/homewizard/translations/zh-Hant.json b/homeassistant/components/homewizard/translations/zh-Hant.json index 3b16abc78f92d..d68bd74668cc8 100644 --- a/homeassistant/components/homewizard/translations/zh-Hant.json +++ b/homeassistant/components/homewizard/translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "api_not_enabled": "API \u672a\u958b\u555f\u3002\u8acb\u65bc HomeWizard Energy App \u8a2d\u5b9a\u5167\u555f\u7528 API", "device_not_supported": "\u4e0d\u652f\u63f4\u6b64\u88dd\u7f6e", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "\u5075\u6e2c\u5230\u4e0d\u652f\u63f4 API \u7248\u672c", "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 7dc7c602b9804..70723fc367699 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": ["pyhomeworks==0.0.6"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyhomeworks"] } diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 68a005c5f7225..bafd4c470db1d 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: return True -async def update_listener(hass, config) -> None: +async def update_listener(hass: HomeAssistant, config: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(config.entry_id) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 9bf4932a95362..7ea878f074e67 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": ["somecomfort==0.8.0"], "codeowners": ["@rdfurman"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["somecomfort"] } diff --git a/homeassistant/components/honeywell/translations/el.json b/homeassistant/components/honeywell/translations/el.json new file mode 100644 index 0000000000000..7ad0c5b0181a2 --- /dev/null +++ b/homeassistant/components/honeywell/translations/el.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "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": { + "data": { + "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": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b1\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (\u0397\u03a0\u0391)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/nb.json b/homeassistant/components/honeywell/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/honeywell/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/pt-BR.json b/homeassistant/components/honeywell/translations/pt-BR.json new file mode 100644 index 0000000000000..f16a6c71637df --- /dev/null +++ b/homeassistant/components/honeywell/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Insira as credenciais usadas para fazer login em mytotalconnectcomfort.com.", + "title": "Honeywell Total Connect Comfort (EUA)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/sk.json b/homeassistant/components/honeywell/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/honeywell/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 09e6066e57372..7a3a2ced5f7b5 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": ["horimote==0.4.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["horimote"] } diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 49f44634bcb01..66d3c84452ac4 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pywebpush==1.9.2"], "dependencies": ["http"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["http_ece", "py_vapid", "pywebpush"] } diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9e77563f7a2e6..a41329a154891 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,22 +1,31 @@ """Support to serve the Home Assistant API as WSGI application.""" from __future__ import annotations -from ipaddress import ip_network +import datetime +from ipaddress import IPv4Network, IPv6Network, ip_network import logging import os import ssl -from typing import Any, Final, Optional, TypedDict, cast +from tempfile import NamedTemporaryFile +from typing import Any, Final, Optional, TypedDict, Union, cast from aiohttp import web from aiohttp.typedefs import StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID import voluptuous as vol +from yarl import URL from homeassistant.components.network import async_get_source_ip from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import async_start_setup, async_when_setup_or_start @@ -59,7 +68,7 @@ DEFAULT_CORS: Final[list[str]] = ["https://cast.home-assistant.io"] NO_LOGIN_ATTEMPT_THRESHOLD: Final = -1 -MAX_CLIENT_SIZE: Final = 1024 ** 2 * 16 +MAX_CLIENT_SIZE: Final = 1024**2 * 16 STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 @@ -109,7 +118,7 @@ class ConfData(TypedDict, total=False): ssl_key: str cors_allowed_origins: list[str] use_x_forwarded_for: bool - trusted_proxies: list[str] + trusted_proxies: list[IPv4Network | IPv6Network] login_attempts_threshold: int ip_ban_enabled: bool ssl_profile: str @@ -216,7 +225,7 @@ def __init__( ssl_key: str | None, server_host: list[str] | None, server_port: int, - trusted_proxies: list[str], + trusted_proxies: list[IPv4Network | IPv6Network], ssl_profile: str, ) -> None: """Initialize the HTTP Home Assistant server.""" @@ -231,6 +240,7 @@ def __init__( self.ssl_profile = ssl_profile self.runner: web.AppRunner | None = None self.site: HomeAssistantTCPSite | None = None + self.context: ssl.SSLContext | None = None async def async_initialize( self, @@ -258,6 +268,11 @@ async def async_initialize( setup_cors(self.app, cors_origins) + if self.ssl_certificate: + self.context = await self.hass.async_add_executor_job( + self._create_ssl_context + ) + def register_view(self, view: HomeAssistantView | type[HomeAssistantView]) -> None: """Register a view with the WSGI server. @@ -329,35 +344,100 @@ async def serve_file(request: web.Request) -> web.FileResponse: self.app.router.add_route("GET", url_path, serve_file) ) - async def start(self) -> None: - """Start the aiohttp server.""" - context: ssl.SSLContext | None - if self.ssl_certificate: + def _create_ssl_context(self) -> ssl.SSLContext | None: + context: ssl.SSLContext | None = None + assert self.ssl_certificate is not None + try: + if self.ssl_profile == SSL_INTERMEDIATE: + context = ssl_util.server_context_intermediate() + else: + context = ssl_util.server_context_modern() + context.load_cert_chain(self.ssl_certificate, self.ssl_key) + except OSError as error: + if not self.hass.config.safe_mode: + raise HomeAssistantError( + f"Could not use SSL certificate from {self.ssl_certificate}: {error}" + ) from error + _LOGGER.error( + "Could not read SSL certificate from %s: %s", + self.ssl_certificate, + error, + ) try: - if self.ssl_profile == SSL_INTERMEDIATE: - context = ssl_util.server_context_intermediate() - else: - context = ssl_util.server_context_modern() - await self.hass.async_add_executor_job( - context.load_cert_chain, self.ssl_certificate, self.ssl_key - ) + context = self._create_emergency_ssl_context() except OSError as error: _LOGGER.error( - "Could not read SSL certificate from %s: %s", - self.ssl_certificate, + "Could not create an emergency self signed ssl certificate: %s", error, ) - return + context = None + else: + _LOGGER.critical( + "Home Assistant is running in safe mode with an emergency self signed ssl certificate because the configured SSL certificate was not usable" + ) + return context - if self.ssl_peer_certificate: - context.verify_mode = ssl.CERT_REQUIRED - await self.hass.async_add_executor_job( - context.load_verify_locations, self.ssl_peer_certificate + if self.ssl_peer_certificate: + if context is None: + raise HomeAssistantError( + "Failed to create ssl context, no fallback available because a peer certificate is required." ) - else: - context = None + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(self.ssl_peer_certificate) + + return context + + def _create_emergency_ssl_context(self) -> ssl.SSLContext: + """Create an emergency ssl certificate so we can still startup.""" + context = ssl_util.server_context_modern() + host: str + try: + host = cast(str, URL(get_url(self.hass, prefer_external=True)).host) + except NoURLAvailableError: + host = "homeassistant.local" + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + subject = issuer = x509.Name( + [ + x509.NameAttribute( + NameOID.ORGANIZATION_NAME, "Home Assistant Emergency Certificate" + ), + x509.NameAttribute(NameOID.COMMON_NAME, host), + ] + ) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=30)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(host)]), + critical=False, + ) + .sign(key, hashes.SHA256()) + ) + with NamedTemporaryFile() as cert_pem, NamedTemporaryFile() as key_pem: + cert_pem.write(cert.public_bytes(serialization.Encoding.PEM)) + key_pem.write( + key.private_bytes( + serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + cert_pem.flush() + key_pem.flush() + context.load_cert_chain(cert_pem.name, key_pem.name) + return context + async def start(self) -> None: + """Start the aiohttp server.""" # Aiohttp freezes apps after start so that no changes can be made. # However in Home Assistant components can be discovered after boot. # This will now raise a RunTimeError. @@ -369,7 +449,7 @@ async def start(self) -> None: await self.runner.setup() self.site = HomeAssistantTCPSite( - self.runner, self.server_host, self.server_port, ssl_context=context + self.runner, self.server_host, self.server_port, ssl_context=self.context ) try: await self.site.start() @@ -399,7 +479,8 @@ async def start_http_server_and_save_config( if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ - str(ip.network_address) for ip in conf[CONF_TRUSTED_PROXIES] + str(cast(Union[IPv4Network, IPv6Network], ip).network_address) + for ip in conf[CONF_TRUSTED_PROXIES] ] store.async_delay_save(lambda: conf, SAVE_DELAY) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index b50555b984123..292c46e55f9cb 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -6,7 +6,7 @@ from contextlib import suppress from datetime import datetime from http import HTTPStatus -from ipaddress import ip_address +from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror from typing import Any, Final @@ -189,7 +189,11 @@ async def process_success_login(request: Request) -> None: class IpBan: """Represents banned IP address.""" - def __init__(self, ip_ban: str, banned_at: datetime | None = None) -> None: + def __init__( + self, + ip_ban: str | IPv4Address | IPv6Address, + banned_at: datetime | None = None, + ) -> None: """Initialize IP Ban object.""" self.ip_address = ip_address(ip_ban) self.banned_at = banned_at or dt_util.utcnow() diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index ff50e9bd9658f..c0aaa31fab014 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable -from ipaddress import ip_address +from ipaddress import IPv4Network, IPv6Network, ip_address import logging from types import ModuleType from typing import Literal @@ -17,7 +17,9 @@ @callback def async_setup_forwarded( - app: Application, use_x_forwarded_for: bool | None, trusted_proxies: list[str] + app: Application, + use_x_forwarded_for: bool | None, + trusted_proxies: list[IPv4Network | IPv6Network], ) -> None: """Create forwarded middleware for the app. diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 6f7ff77efb78c..c554c775079bd 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["i2csense", "smbus"] } diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 9cfc008921bf1..3e7ebf24b1637 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -15,5 +15,6 @@ } ], "codeowners": ["@scop", "@fphammerle"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["huawei_lte_api"] } diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index f5e6f8763c992..0152640016527 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -1,17 +1,28 @@ { "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", "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE" }, "error": { "connection_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_password": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "incorrect_username": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "invalid_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "login_attempts_exceeded": "\u03a5\u03c0\u03ad\u03c1\u03b2\u03b1\u03c3\u03b7 \u03c4\u03c9\u03bd \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03b9\u03ce\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "response_error": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name}", "step": { "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Huawei LTE" } @@ -24,7 +35,8 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 (\u03b7 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7)", "recipient": "\u03a0\u03b1\u03c1\u03b1\u03bb\u03ae\u03c0\u03c4\u03b5\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd SMS", "track_new_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bd\u03ad\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd", - "track_wired_clients": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c0\u03b5\u03bb\u03b1\u03c4\u03ce\u03bd \u03b5\u03bd\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + "track_wired_clients": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c0\u03b5\u03bb\u03b1\u03c4\u03ce\u03bd \u03b5\u03bd\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "unauthenticated_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 (\u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7)" } } } diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index c69337fa2bb33..7b69fce212d8a 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -1,13 +1,42 @@ { "config": { "abort": { - "already_configured": "Este dispositivo j\u00e1 foi configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE" }, + "error": { + "connection_timeout": "Tempo limite de conex\u00e3o atingido", + "incorrect_password": "Senha incorreta", + "incorrect_username": "Nome de usu\u00e1rio incorreto", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_url": "URL inv\u00e1lida", + "login_attempts_exceeded": "O m\u00e1ximo de tentativas de login foi excedido. Tente novamente mais tarde", + "response_error": "Erro desconhecido do dispositivo", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "password": "Senha", "url": "URL", "username": "Usu\u00e1rio" + }, + "description": "Digite os detalhes de acesso do dispositivo.", + "title": "Configurar Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nome do servi\u00e7o de notifica\u00e7\u00e3o (a altera\u00e7\u00e3o requer rein\u00edcio)", + "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS", + "track_new_devices": "Rastrear novos dispositivos", + "track_wired_clients": "Rastrear clientes da rede cabeada", + "unauthenticated_mode": "Modo n\u00e3o autenticado (a altera\u00e7\u00e3o requer recarga)" } } } diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json new file mode 100644 index 0000000000000..0b7bf878ea988 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 987afe17012ae..0901d9a1e2c28 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -3,6 +3,7 @@ import asyncio import logging +from typing import Any from urllib.parse import urlparse from aiohue import LinkButtonNotPressed, create_app_key @@ -19,7 +20,6 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ALLOW_HUE_GROUPS, @@ -59,7 +59,9 @@ def __init__(self) -> None: self.bridge: DiscoveredHueBridge | None = None self.discovered_bridges: dict[str, DiscoveredHueBridge] | None = None - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" # This is for backwards compatibility. return await self.async_step_init(user_input) @@ -76,7 +78,9 @@ async def _get_bridge( assert bridge_id == bridge.id return bridge - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID: @@ -126,7 +130,7 @@ async def async_step_init(self, user_input: ConfigType | None = None) -> FlowRes ) async def async_step_manual( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle manual bridge setup.""" if user_input is None: @@ -139,7 +143,9 @@ async def async_step_manual( self.bridge = await self._get_bridge(user_input[CONF_HOST]) return await self.async_step_link() - async def async_step_link(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Attempt to link with the Hue bridge. Given a configured host, will ask the user to press the link button @@ -268,7 +274,7 @@ async def async_step_homekit( await self._async_handle_discovery_without_unique_id() return await self.async_step_link() - async def async_step_import(self, import_info: ConfigType) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: """Import a new bridge as a config entry. This flow is triggered by `async_setup` for both configured and @@ -291,7 +297,9 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Hue options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -324,7 +332,9 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Hue options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index ee0453c9da684..a4b545aa14131 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -41,8 +41,7 @@ async def async_validate_trigger_config(hass: "HomeAssistant", config: ConfigTyp device_id = config[CONF_DEVICE_ID] # lookup device in HASS DeviceRegistry dev_reg: dr.DeviceRegistry = dr.async_get(hass) - device_entry = dev_reg.async_get(device_id) - if device_entry is None: + if (device_entry := dev_reg.async_get(device_id)) is None: raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") for conf_entry_id in device_entry.config_entries: @@ -64,8 +63,7 @@ async def async_attach_trigger( device_id = config[CONF_DEVICE_ID] # lookup device in HASS DeviceRegistry dev_reg: dr.DeviceRegistry = dr.async_get(hass) - device_entry = dev_reg.async_get(device_id) - if device_entry is None: + if (device_entry := dev_reg.async_get(device_id)) is None: raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") for conf_entry_id in device_entry.config_entries: @@ -90,8 +88,7 @@ async def async_get_triggers(hass: "HomeAssistant", device_id: str): return [] # lookup device in HASS DeviceRegistry dev_reg: dr.DeviceRegistry = dr.async_get(hass) - device_entry = dev_reg.async_get(device_id) - if device_entry is None: + if (device_entry := dev_reg.async_get(device_id)) is None: raise ValueError(f"Device ID {device_id} is not valid") # Iterate all config entries for this device diff --git a/homeassistant/components/hue/diagnostics.py b/homeassistant/components/hue/diagnostics.py new file mode 100644 index 0000000000000..17f00a50bbe13 --- /dev/null +++ b/homeassistant/components/hue/diagnostics.py @@ -0,0 +1,22 @@ +"""Diagnostics support for Hue.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .bridge import HueBridge +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + bridge: HueBridge = hass.data[DOMAIN][entry.entry_id] + if bridge.api_version == 1: + # diagnostics is only implemented for V2 bridges. + return {} + # Hue diagnostics are already redacted + return await bridge.api.get_diagnostics() diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 318dacd344988..bf6e7f06abdc0 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.2.1"], + "requirements": ["aiohue==4.3.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", @@ -24,5 +24,6 @@ "zeroconf": ["_hue._tcp.local."], "codeowners": ["@balloob", "@marcelveldt"], "quality_scale": "platinum", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiohue"] } diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index f779fccdb3b6a..1d56d493785db 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -39,8 +39,7 @@ async def check_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: data[CONF_API_KEY] = data.pop(CONF_USERNAME) hass.config_entries.async_update_entry(entry, data=data) - conf_api_version = entry.data.get(CONF_API_VERSION, 1) - if conf_api_version == 1: + if (conf_api_version := entry.data.get(CONF_API_VERSION, 1)) == 1: # a bridge might have upgraded firmware since last run so # we discover its capabilities at every startup websession = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/hue/translations/cs.json b/homeassistant/components/hue/translations/cs.json index 1708abfe75017..cee67307991d3 100644 --- a/homeassistant/components/hue/translations/cs.json +++ b/homeassistant/components/hue/translations/cs.json @@ -35,6 +35,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "Prvn\u00ed tla\u010d\u00edtko", + "2": "Druh\u00e9 tla\u010d\u00edtko", + "3": "T\u0159et\u00ed tla\u010d\u00edtko", + "4": "\u010ctvrt\u00e9 tla\u010d\u00edtko", "button_1": "Prvn\u00ed tla\u010d\u00edtko", "button_2": "Druh\u00e9 tla\u010d\u00edtko", "button_3": "T\u0159et\u00ed tla\u010d\u00edtko", @@ -47,11 +51,16 @@ "turn_on": "Zapnout" }, "trigger_type": { + "double_short_release": "Oba \"{subtype}\" uvoln\u011bny", + "initial_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", + "long_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po dlouh\u00e9m stisku", "remote_button_long_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po dlouh\u00e9m stisku", "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", "remote_double_button_long_press": "Oba \"{subtype}\" uvoln\u011bny po dlouh\u00e9m stisku", - "remote_double_button_short_press": "Oba \"{subtype}\" uvoln\u011bny" + "remote_double_button_short_press": "Oba \"{subtype}\" uvoln\u011bny", + "repeat": "Tla\u010d\u00edtko \"{subtype}\" podr\u017eeno", + "short_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po kr\u00e1tk\u00e9m stisku" } }, "options": { @@ -59,7 +68,9 @@ "init": { "data": { "allow_hue_groups": "Povolit skupiny Hue", - "allow_unreachable": "Povolit nedostupn\u00fdm \u017e\u00e1rovk\u00e1m spr\u00e1vn\u011b hl\u00e1sit jejich stav" + "allow_hue_scenes": "Povolit sc\u00e9ny Hue", + "allow_unreachable": "Povolit nedostupn\u00fdm \u017e\u00e1rovk\u00e1m spr\u00e1vn\u011b hl\u00e1sit jejich stav", + "ignore_availability": "Ignorovat stav spojen\u00ed pro dan\u00e9 za\u0159\u00edzen\u00ed" } } } diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 56cc2ce2752f1..99bdc934929ae 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -1,7 +1,36 @@ { "config": { "abort": { - "not_hue_bridge": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 Hue" + "all_configured": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b5\u03c2 Philips Hue \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "discover_timeout": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03b3\u03b5\u03c6\u03c5\u03c1\u03ce\u03bd Hue", + "no_bridges": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b3\u03ad\u03c6\u03c5\u03c1\u03b5\u03c2 Philips Hue", + "not_hue_bridge": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 Hue", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "linking": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "register_failed": "\u0397 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac" + }, + "step": { + "init": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 Hue" + }, + "link": { + "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Philips Hue \u03bc\u03b5 \u03c4\u03bf Home Assistant. \n\n ![\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd \u03c3\u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1](/static/images/config_philips_hue.jpg)", + "title": "\u039a\u03cc\u03bc\u03b2\u03bf\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c9\u03bd" + }, + "manual": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03bc\u03b9\u03b1\u03c2 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1\u03c2 Hue" + } } }, "device_automation": { @@ -22,16 +51,25 @@ "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" }, "trigger_type": { + "double_short_release": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd", + "initial_press": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac", + "long_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", "remote_button_long_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", "remote_button_short_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"", - "remote_button_short_release": "\u0391\u03c6\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"" + "remote_button_short_release": "\u0391\u03c6\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c4\u03bf\u03c5 \"{subtype}\"", + "remote_double_button_long_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", + "remote_double_button_short_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd", + "repeat": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03ba\u03c1\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c0\u03b1\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf", + "short_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \" {subtype} \" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1" } }, "options": { "step": { "init": { "data": { + "allow_hue_groups": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2 Hue", "allow_hue_scenes": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03ba\u03b7\u03bd\u03ad\u03c2 Hue", + "allow_unreachable": "\u0395\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03c3\u03c4\u03bf\u03c5\u03c2 \u03bc\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf\u03c5\u03c2 \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b5\u03c2 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03bf\u03c5\u03bd \u03c3\u03c9\u03c3\u03c4\u03ac \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2", "ignore_availability": "\u03a0\u03b1\u03c1\u03ac\u03b2\u03bb\u03b5\u03c8\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2" } } diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index 1084d980ea5d1..d7178f6d135e4 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -69,7 +69,8 @@ "data": { "allow_hue_groups": "Izinkan grup Hue", "allow_hue_scenes": "Izinkan skenario Hue", - "allow_unreachable": "Izinkan bohlam yang tidak dapat dijangkau untuk melaporkan statusnya dengan benar" + "allow_unreachable": "Izinkan bohlam yang tidak dapat dijangkau untuk melaporkan statusnya dengan benar", + "ignore_availability": "Abaikan status konektivitas untuk perangkat yang diberikan" } } } diff --git a/homeassistant/components/hue/translations/pt-BR.json b/homeassistant/components/hue/translations/pt-BR.json index 9a7e8094b1131..05dec67831834 100644 --- a/homeassistant/components/hue/translations/pt-BR.json +++ b/homeassistant/components/hue/translations/pt-BR.json @@ -2,35 +2,77 @@ "config": { "abort": { "all_configured": "Todas as pontes Philips Hue j\u00e1 est\u00e3o configuradas", - "already_configured": "A ponte j\u00e1 est\u00e1 configurada", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o da ponte j\u00e1 est\u00e1 em andamento.", - "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", "discover_timeout": "Incapaz de descobrir pontes Hue", "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas", "not_hue_bridge": "N\u00e3o \u00e9 uma ponte Hue", - "unknown": "Ocorreu um erro desconhecido" + "unknown": "Erro inesperado" }, "error": { - "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "linking": "Erro inesperado", "register_failed": "Falhou ao registrar, por favor tente novamente" }, "step": { "init": { "data": { - "host": "Hospedeiro" + "host": "Nome do host" }, "title": "Escolha a ponte Hue" }, "link": { "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte](/static/images/config_philips_hue.jpg)", "title": "Hub de links" + }, + "manual": { + "data": { + "host": "Nome do host" + }, + "title": "Configurar manualmente uma ponte Hue" } } }, "device_automation": { "trigger_subtype": { + "1": "Primeiro bot\u00e3o", + "2": "Segundo bot\u00e3o", + "3": "Terceiro bot\u00e3o", + "4": "Quarto bot\u00e3o", + "button_1": "Primeiro bot\u00e3o", + "button_2": "Segundo bot\u00e3o", + "button_3": "Terceiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o", + "dim_down": "Diminuir a luminosidade", + "dim_up": "Aumentar a luminosidade", "double_buttons_1_3": "Primeiro e terceiro bot\u00f5es", - "double_buttons_2_4": "Segundo e quarto bot\u00f5es" + "double_buttons_2_4": "Segundo e quarto bot\u00f5es", + "turn_off": "Desligar", + "turn_on": "Ligar" + }, + "trigger_type": { + "double_short_release": "Ambos \"{subtype}\" liberados", + "initial_press": "Bot\u00e3o \" {subtype} \" pressionado inicialmente", + "long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", + "remote_button_long_release": "Bot\u00e3o \"{subtype}\" liberado ap\u00f3s longa press\u00e3o", + "remote_button_short_press": "Bot\u00e3o \"{subtype}\" pressionado", + "remote_button_short_release": "Bot\u00e3o \"{subtype}\" liberado", + "remote_double_button_long_press": "Ambos \"{subtype}\" lan\u00e7ados ap\u00f3s longa imprensa", + "remote_double_button_short_press": "Ambos \"{subtype}\" lan\u00e7ados", + "repeat": "Bot\u00e3o \" {subtype} \" pressionado", + "short_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s pressionamento curto" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Permitir grupos Hue", + "allow_hue_scenes": "Permitir cenas Hue", + "allow_unreachable": "Permitir que l\u00e2mpadas inacess\u00edveis relatem seu estado corretamente", + "ignore_availability": "Ignorar o status de conectividade para os dispositivos fornecidos" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json index 3912c15c86c66..424ac0d9252ba 100644 --- a/homeassistant/components/hue/translations/sk.json +++ b/homeassistant/components/hue/translations/sk.json @@ -2,11 +2,22 @@ "config": { "abort": { "all_configured": "V\u0161etky Philips Hue bridge u\u017e boli nakonfigurovan\u00e9", - "no_bridges": "Neboli objaven\u00fd \u017eiaden Philips Hue bridge" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "no_bridges": "Neboli objaven\u00fd \u017eiaden Philips Hue bridge", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "error": { + "linking": "Neo\u010dak\u00e1van\u00e1 chyba", + "register_failed": "Registr\u00e1cia zlyhala, pros\u00edm, sk\u00faste znova" }, "step": { + "init": { + "title": "Vyberte Hue bridge" + }, "link": { - "description": "Stla\u010dte tla\u010didlo na Philips Hue bridge pre registr\u00e1ciu Philips Hue s Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" + "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)" } } } diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 3f474cdf70b14..cab21b63d6d5f 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -72,7 +72,7 @@ def check_invalid_device_trigger( "Please manually fix the outdated automation(s) once to fix this issue." ) if automation_info: - automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore + automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore[index] msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})." persistent_notification.async_create( bridge.hass, diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 1d45293012cfd..4b9adf1622646 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -47,7 +47,7 @@ def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: data = { # send slugified entity name as id = backwards compatibility with previous version CONF_ID: slugify(f"{hue_device.metadata.name} Button"), - CONF_DEVICE_ID: device.id, # type: ignore + CONF_DEVICE_ID: device.id, # type: ignore[union-attr] CONF_UNIQUE_ID: hue_resource.id, CONF_TYPE: hue_resource.button.last_event.value, CONF_SUBTYPE: hue_resource.metadata.control_id, diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 6b9981fee2379..8640f126ae41a 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@dennisschroer" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["huisbaasje"] } \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/el.json b/homeassistant/components/huisbaasje/translations/el.json new file mode 100644 index 0000000000000..5b1861a0e4080 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/el.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/pt-BR.json b/homeassistant/components/huisbaasje/translations/pt-BR.json new file mode 100644 index 0000000000000..66c671f99a3c2 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/sk.json b/homeassistant/components/huisbaasje/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index d01479fbd8764..e9c8bb02df978 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { + "changed_states": "{entity_name} activado o desactivado", "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 7f409ca75d65b..3b1b60ebae339 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { + "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "target_humidity_changed": "{nom_de_l'entit\u00e9} changement de l'humidit\u00e9 cible", "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} s'est \u00e9teint", diff --git a/homeassistant/components/humidifier/translations/id.json b/homeassistant/components/humidifier/translations/id.json index b06b2bfee45aa..1996fd23786b5 100644 --- a/homeassistant/components/humidifier/translations/id.json +++ b/homeassistant/components/humidifier/translations/id.json @@ -13,7 +13,9 @@ "is_on": "{entity_name} nyala" }, "trigger_type": { + "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", "target_humidity_changed": "Kelembapan target {entity_name} berubah", + "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/humidifier/translations/nl.json b/homeassistant/components/humidifier/translations/nl.json index 9505a6a083861..8d96d69820901 100644 --- a/homeassistant/components/humidifier/translations/nl.json +++ b/homeassistant/components/humidifier/translations/nl.json @@ -13,7 +13,9 @@ "is_on": "{entity_name} staat aan" }, "trigger_type": { + "changed_states": "{entity_name} in- of uitgeschakeld", "target_humidity_changed": "{entity_name} doel luchtvochtigheid gewijzigd", + "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} is uitgeschakeld", "turned_on": "{entity_name} is ingeschakeld" } diff --git a/homeassistant/components/humidifier/translations/pl.json b/homeassistant/components/humidifier/translations/pl.json index 34e67bcf73e72..5c82a1e944bd9 100644 --- a/homeassistant/components/humidifier/translations/pl.json +++ b/homeassistant/components/humidifier/translations/pl.json @@ -13,6 +13,7 @@ "is_on": "nawil\u017cacz {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { + "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "target_humidity_changed": "zmieni si\u0119 wilgotno\u015b\u0107 docelowa {entity_name}", "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", diff --git a/homeassistant/components/humidifier/translations/pt-BR.json b/homeassistant/components/humidifier/translations/pt-BR.json new file mode 100644 index 0000000000000..bb4b6c00177bd --- /dev/null +++ b/homeassistant/components/humidifier/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Definir umidade para {entity_name}", + "set_mode": "Alterar modo em {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} est\u00e1 definido para um modo espec\u00edfico", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "changed_states": "{entity_name} for ligado ou desligado", + "target_humidity_changed": "{entity_name} tiver a umidade alvo alterada", + "toggled": "{entity_name} for ligado ou desligado", + "turned_off": "{entity_name} for desligado", + "turned_on": "{entity_name} for ligado" + } + }, + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + } + }, + "title": "Umidificador" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index ade3b25f31c88..4e1ece9a3b008 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -9,11 +9,13 @@ "models": ["PowerView"] }, "dhcp": [ + {"registered_devices": true}, { "hostname": "hunter*", "macaddress": "002674*" } ], "zeroconf": ["_powerview._tcp.local."], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiopvapi"] } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/el.json b/homeassistant/components/hunterdouglas_powerview/translations/el.json new file mode 100644 index 0000000000000..40bdfa3c7d37c --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/el.json @@ -0,0 +1,24 @@ +{ + "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" + }, + "flow_title": "{name} ({host})", + "step": { + "link": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf PowerView Hub" + }, + "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf PowerView Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/it.json b/homeassistant/components/hunterdouglas_powerview/translations/it.json index df027146b12b9..6a5606b05f405 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/it.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/it.json @@ -11,13 +11,13 @@ "step": { "link": { "description": "Vuoi impostare {name} ({host})?", - "title": "Connettersi all'Hub PowerView" + "title": "Connettiti all'Hub PowerView" }, "user": { "data": { "host": "Indirizzo IP" }, - "title": "Collegamento al PowerView Hub" + "title": "Connettiti al PowerView Hub" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json b/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json index f7dc708a2d616..b170cb598828f 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ( {host} )", "step": { + "link": { + "description": "Deseja configurar {name} ( {host} )?", + "title": "Conecte-se ao PowerView Hub" + }, "user": { "data": { "host": "Endere\u00e7o IP" - } + }, + "title": "Conecte-se ao PowerView Hub" } } } diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index 71a6abdfbdd54..f0334b5af9206 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/hvv_departures", "requirements": ["pygti==0.9.2"], "codeowners": ["@vigonotion"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pygti"] } diff --git a/homeassistant/components/hvv_departures/translations/el.json b/homeassistant/components/hvv_departures/translations/el.json new file mode 100644 index 0000000000000..91bf7e078689b --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/el.json @@ -0,0 +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": { + "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", + "no_results": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bc\u03b5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc/\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + }, + "step": { + "station": { + "data": { + "station": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2/\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + }, + "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc/\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + }, + "station_select": { + "data": { + "station": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2/\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc/\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + }, + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf HVV API" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03c2", + "offset": "\u039c\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 (\u03bb\u03b5\u03c0\u03c4\u03ac)", + "real_time": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf" + }, + "description": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b1\u03bd\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/pt-BR.json b/homeassistant/components/hvv_departures/translations/pt-BR.json new file mode 100644 index 0000000000000..325caaa27f69e --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/pt-BR.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_results": "Sem resultados. Tente com uma esta\u00e7\u00e3o/endere\u00e7o diferente" + }, + "step": { + "station": { + "data": { + "station": "Esta\u00e7\u00e3o/Endere\u00e7o" + }, + "title": "Digite Esta\u00e7\u00e3o/Endere\u00e7o" + }, + "station_select": { + "data": { + "station": "Esta\u00e7\u00e3o/Endere\u00e7o" + }, + "title": "Selecione Esta\u00e7\u00e3o/Endere\u00e7o" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se \u00e0 API HVV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "Selecionar linhas", + "offset": "Deslocamento (minutos)", + "real_time": "Usar dados em tempo real" + }, + "description": "Alterar op\u00e7\u00f5es para este sensor de partida", + "title": "Op\u00e7\u00f5es" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sk.json b/homeassistant/components/hvv_departures/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index e9656b69eb81b..8db827a8c35c4 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": ["hydrawiser==0.2"], "codeowners": ["@ptcryan"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["hydrawiser"] } diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 4f247b3e937c2..8a886053361d7 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -12,5 +12,6 @@ "st": "urn:hyperion-project.org:device:basic:1" } ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["hyperion"] } diff --git a/homeassistant/components/hyperion/translations/el.json b/homeassistant/components/hyperion/translations/el.json index c9e64b96906b9..6c55ac02969da 100644 --- a/homeassistant/components/hyperion/translations/el.json +++ b/homeassistant/components/hyperion/translations/el.json @@ -1,14 +1,52 @@ { "config": { "abort": { - "auth_new_token_not_granted_error": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03bf token \u03b4\u03b5\u03bd \u03b5\u03b3\u03ba\u03c1\u03af\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf Hyperion UI" + "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", + "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", + "auth_new_token_not_granted_error": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03bf token \u03b4\u03b5\u03bd \u03b5\u03b3\u03ba\u03c1\u03af\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf Hyperion UI", + "auth_new_token_not_work_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1", + "auth_required_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03b5\u03ac\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_id": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 Hyperion Ambilight \u03b4\u03b5\u03bd \u03b1\u03bd\u03ad\u03c6\u03b5\u03c1\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03b7\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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "step": { + "auth": { + "data": { + "create_token": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd", + "token": "\u0389 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03cb\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Hyperion Ambilight" + }, + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf Hyperion Ambilight \u03c3\u03c4\u03bf Home Assistant; \n\n **\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2:** {host}\n **\u0398\u03cd\u03c1\u03b1:** {port}\n **\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc**: {id}", + "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1\u03c2 Hyperion Ambilight" + }, + "create_token": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae** \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03ad\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0398\u03b1 \u03b1\u03bd\u03b1\u03ba\u03b1\u03c4\u03b5\u03c5\u03b8\u03c5\u03bd\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Hyperion UI \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03c1\u03af\u03bd\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \" {auth_id} \"", + "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bd\u03ad\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "create_token_external": { + "title": "\u0391\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03bd\u03ad\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03bf Hyperion UI" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } } }, "options": { "step": { "init": { "data": { - "effect_show_list": "\u0395\u03c6\u03ad Hyperion \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7" + "effect_show_list": "\u0395\u03c6\u03ad Hyperion \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7", + "priority": "\u03a0\u03c1\u03bf\u03c4\u03b5\u03c1\u03b1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 Hyperion \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c7\u03c1\u03c9\u03bc\u03ac\u03c4\u03c9\u03bd \u03ba\u03b1\u03b9 \u03b5\u03c6\u03ad" } } } diff --git a/homeassistant/components/hyperion/translations/pt-BR.json b/homeassistant/components/hyperion/translations/pt-BR.json new file mode 100644 index 0000000000000..a0d75722f0d37 --- /dev/null +++ b/homeassistant/components/hyperion/translations/pt-BR.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "auth_new_token_not_granted_error": "O token rec\u00e9m-criado n\u00e3o foi aprovado na interface do usu\u00e1rio do Hyperion", + "auth_new_token_not_work_error": "Falha ao autenticar usando o token rec\u00e9m-criado", + "auth_required_error": "Falha ao determinar se a autoriza\u00e7\u00e3o \u00e9 necess\u00e1ria", + "cannot_connect": "Falha ao conectar", + "no_id": "A inst\u00e2ncia Hyperion Ambilight n\u00e3o informou seu ID", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, + "step": { + "auth": { + "data": { + "create_token": "Criar novo token automaticamente", + "token": "Ou forne\u00e7a um token pr\u00e9-existente" + }, + "description": "Configure a autoriza\u00e7\u00e3o para seu servidor Hyperion Ambilight" + }, + "confirm": { + "description": "Deseja adicionar este Hyperion Ambilight ao Home Assistant?\n\n**Host:** {host}\n**Porta:** {port}\n**ID**: {id}", + "title": "Confirme a adi\u00e7\u00e3o do servi\u00e7o Hyperion Ambilight" + }, + "create_token": { + "description": "Escolha **Enviar** abaixo para solicitar um novo token de autentica\u00e7\u00e3o. Voc\u00ea ser\u00e1 redirecionado para a interface do usu\u00e1rio do Hyperion para aprovar a solicita\u00e7\u00e3o. Verifique se o ID mostrado \u00e9 \" {auth_id} \"", + "title": "Criar automaticamente um novo token de autentica\u00e7\u00e3o" + }, + "create_token_external": { + "title": "Aceitar novo token na interface do usu\u00e1rio do Hyperion" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Efeitos do Hyperion para mostrar", + "priority": "Prioridade do Hyperion a ser usada para cores e efeitos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/sk.json b/homeassistant/components/hyperion/translations/sk.json new file mode 100644 index 0000000000000..a8223b5b2e362 --- /dev/null +++ b/homeassistant/components/hyperion/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 751faec56c717..60ecb9da74ab8 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyialarm==1.9.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyialarm"] } diff --git a/homeassistant/components/ialarm/translations/el.json b/homeassistant/components/ialarm/translations/el.json new file mode 100644 index 0000000000000..4a3011c880b1f --- /dev/null +++ b/homeassistant/components/ialarm/translations/el.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/pt-BR.json b/homeassistant/components/ialarm/translations/pt-BR.json new file mode 100644 index 0000000000000..1e898e15ce055 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/sk.json b/homeassistant/components/ialarm/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/ialarm/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iammeter/manifest.json b/homeassistant/components/iammeter/manifest.json index e0e0b68bcf454..2263b583dddad 100644 --- a/homeassistant/components/iammeter/manifest.json +++ b/homeassistant/components/iammeter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/iammeter", "codeowners": ["@lewei50"], "requirements": ["iammeter==0.1.7"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["iammeter"] } diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 73aa6f188677f..030bb8cdcc8fe 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -71,9 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Aqualink component.""" - conf = config.get(DOMAIN) - - if conf is not None: + if (conf := config.get(DOMAIN)) is not None: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index a91964ba3bc0e..921102b85dc07 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -12,7 +12,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -56,6 +55,6 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None): errors=errors, ) - async def async_step_import(self, user_input: ConfigType | None = None): + async def async_step_import(self, user_input: dict[str, Any] | None = None): """Occurs when an entry is setup through config.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 8061163943daf..7c57744fd3b0a 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], "requirements": ["iaqualink==0.4.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["iaqualink"] } diff --git a/homeassistant/components/iaqualink/translations/el.json b/homeassistant/components/iaqualink/translations/el.json new file mode 100644 index 0000000000000..e9dc0dfa39b46 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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", + "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": { + "data": { + "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": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf iAqualink.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf iAqualink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/it.json b/homeassistant/components/iaqualink/translations/it.json index 5947717e69b4c..2337bbd42163e 100644 --- a/homeassistant/components/iaqualink/translations/it.json +++ b/homeassistant/components/iaqualink/translations/it.json @@ -14,7 +14,7 @@ "username": "Nome utente" }, "description": "Inserisci il nome utente e la password del tuo account iAqualink.", - "title": "Collegati a iAqualink" + "title": "Connettiti a iAqualink" } } } diff --git a/homeassistant/components/iaqualink/translations/pt-BR.json b/homeassistant/components/iaqualink/translations/pt-BR.json index 932b4b8a72e0a..bdf8cc5c5bdd2 100644 --- a/homeassistant/components/iaqualink/translations/pt-BR.json +++ b/homeassistant/components/iaqualink/translations/pt-BR.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { + "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "description": "Por favor, digite o nome de usu\u00e1rio e senha para sua conta iAqualink.", + "title": "Conecte-se ao iAqualink" } } } diff --git a/homeassistant/components/iaqualink/translations/sk.json b/homeassistant/components/iaqualink/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/uk.json b/homeassistant/components/iaqualink/translations/uk.json index b855d75572603..fc71e748a157d 100644 --- a/homeassistant/components/iaqualink/translations/uk.json +++ b/homeassistant/components/iaqualink/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" diff --git a/homeassistant/components/iaqualink/translations/zh-Hant.json b/homeassistant/components/iaqualink/translations/zh-Hant.json index b6c25038bdb06..13591ed948503 100644 --- a/homeassistant/components/iaqualink/translations/zh-Hant.json +++ b/homeassistant/components/iaqualink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 6c40ef6bf03b2..168eafe70473b 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,7 +3,8 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.10.2"], + "requirements": ["pyicloud==1.0.0"], "codeowners": ["@Quentame", "@nzapponi"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["keyrings.alt", "pyicloud"] } diff --git a/homeassistant/components/icloud/translations/el.json b/homeassistant/components/icloud/translations/el.json index 1fa59992bc3aa..cc484bd5660d8 100644 --- a/homeassistant/components/icloud/translations/el.json +++ b/homeassistant/components/icloud/translations/el.json @@ -1,13 +1,45 @@ { "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", + "no_device": "\u039a\u03b1\u03bc\u03af\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"Find my iPhone\".", + "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_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", + "send_verification_code": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "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": { - "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." + "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" + }, + "trusted_device": { + "data": { + "trusted_device": "\u0391\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2", + "title": "\u0391\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae iCloud" }, "user": { "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email", "with_family": "\u039c\u03b5 \u03c4\u03b7\u03bd \u03bf\u03b9\u03ba\u03bf\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1" - } + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2", + "title": "\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03bc\u03cc\u03bb\u03b9\u03c2 \u03bb\u03ac\u03b2\u03b1\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf iCloud", + "title": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 iCloud" } } } diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 7005c83bf6947..0c3363a379905 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -1,13 +1,23 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "no_device": "Nenhum dos seus dispositivos tem \"Encontrar meu iPhone\" ativado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "send_verification_code": "Falha ao enviar c\u00f3digo de verifica\u00e7\u00e3o", - "validate_verification_code": "Falha ao verificar seu c\u00f3digo de verifica\u00e7\u00e3o, escolha um dispositivo confi\u00e1vel e inicie a verifica\u00e7\u00e3o novamente" + "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" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" @@ -18,7 +28,8 @@ "user": { "data": { "password": "Senha", - "username": "E-mail" + "username": "Email", + "with_family": "Com a fam\u00edlia" }, "description": "Insira suas credenciais", "title": "credenciais do iCloud" diff --git a/homeassistant/components/icloud/translations/sk.json b/homeassistant/components/icloud/translations/sk.json new file mode 100644 index 0000000000000..d30ed436a4fd2 --- /dev/null +++ b/homeassistant/components/icloud/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index aa18ead9b6e41..005307b24e18a 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": ["rfk101py==0.0.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["rfk101py"] } diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index a4699853b0174..35daf519769c8 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyfttt==0.3"], "dependencies": ["webhook"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pyfttt"] } diff --git a/homeassistant/components/ifttt/translations/bg.json b/homeassistant/components/ifttt/translations/bg.json index 3a23d735f760b..54d0a83ffcfc7 100644 --- a/homeassistant/components/ifttt/translations/bg.json +++ b/homeassistant/components/ifttt/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/ifttt/translations/ca.json b/homeassistant/components/ifttt/translations/ca.json index 0c8bb89ba02fb..17e7fe938d075 100644 --- a/homeassistant/components/ifttt/translations/ca.json +++ b/homeassistant/components/ifttt/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json index 216511c62f534..905dbbe420dea 100644 --- a/homeassistant/components/ifttt/translations/de.json +++ b/homeassistant/components/ifttt/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/ifttt/translations/el.json b/homeassistant/components/ifttt/translations/el.json index aecb2ee553fa4..0ef5f6df6a4ed 100644 --- a/homeassistant/components/ifttt/translations/el.json +++ b/homeassistant/components/ifttt/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \"\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u0399\u03c3\u03c4\u03bf\u03cd\" \u03b1\u03c0\u03cc \u03c4\u03b7 [\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae IFTTT Webhook]({applet_url}). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: `{webhook_url}`\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/json \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf IFTTT;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 IFTTT Webhook Applet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/en.json b/homeassistant/components/ifttt/translations/en.json index 68999eba2b70e..d7b79b282eb78 100644 --- a/homeassistant/components/ifttt/translations/en.json +++ b/homeassistant/components/ifttt/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json index e4d2c8f14886b..caa82dddd41c8 100644 --- a/homeassistant/components/ifttt/translations/et.json +++ b/homeassistant/components/ifttt/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/ifttt/translations/fr.json b/homeassistant/components/ifttt/translations/fr.json index fe72a0df1725e..4628b7bea8bc1 100644 --- a/homeassistant/components/ifttt/translations/fr.json +++ b/homeassistant/components/ifttt/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/ifttt/translations/he.json b/homeassistant/components/ifttt/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/ifttt/translations/he.json +++ b/homeassistant/components/ifttt/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index 2f64056e985f0..21bfa86dcec58 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/ifttt/translations/id.json b/homeassistant/components/ifttt/translations/id.json index f997f39a54ead..8ddb7c798c11b 100644 --- a/homeassistant/components/ifttt/translations/id.json +++ b/homeassistant/components/ifttt/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/ifttt/translations/it.json b/homeassistant/components/ifttt/translations/it.json index 6b1d1dd4c53c7..93ef3f7dc31c5 100644 --- a/homeassistant/components/ifttt/translations/it.json +++ b/homeassistant/components/ifttt/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 81616a31dd724..a3a63f532cfb0 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/ifttt/translations/nb.json b/homeassistant/components/ifttt/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/ifttt/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/nl.json b/homeassistant/components/ifttt/translations/nl.json index 82006860db3ad..727b21f43be7b 100644 --- a/homeassistant/components/ifttt/translations/nl.json +++ b/homeassistant/components/ifttt/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/ifttt/translations/no.json b/homeassistant/components/ifttt/translations/no.json index 98fec09e773b2..dc10e25686428 100644 --- a/homeassistant/components/ifttt/translations/no.json +++ b/homeassistant/components/ifttt/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/ifttt/translations/pl.json b/homeassistant/components/ifttt/translations/pl.json index d8d7bff158549..21144ed7829a6 100644 --- a/homeassistant/components/ifttt/translations/pl.json +++ b/homeassistant/components/ifttt/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/ifttt/translations/pt-BR.json b/homeassistant/components/ifttt/translations/pt-BR.json index c78da4db0c01e..239afa3b7e48e 100644 --- a/homeassistant/components/ifttt/translations/pt-BR.json +++ b/homeassistant/components/ifttt/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 usar a a\u00e7\u00e3o \"Fazer uma solicita\u00e7\u00e3o Web\" no [applet IFTTT Webhook] ( {applet_url} ). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application / json \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/ifttt/translations/ru.json b/homeassistant/components/ifttt/translations/ru.json index 8cf34ace24efe..8d5fc315beb38 100644 --- a/homeassistant/components/ifttt/translations/ru.json +++ b/homeassistant/components/ifttt/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/ifttt/translations/tr.json b/homeassistant/components/ifttt/translations/tr.json index b42268fa889fc..6585c0245e288 100644 --- a/homeassistant/components/ifttt/translations/tr.json +++ b/homeassistant/components/ifttt/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/ifttt/translations/uk.json b/homeassistant/components/ifttt/translations/uk.json index 8ea8f2b1970da..46bc1de9d62d6 100644 --- a/homeassistant/components/ifttt/translations/uk.json +++ b/homeassistant/components/ifttt/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/ifttt/translations/zh-Hans.json b/homeassistant/components/ifttt/translations/zh-Hans.json index 78cbc37a7d9a9..58ba93cd27686 100644 --- a/homeassistant/components/ifttt/translations/zh-Hans.json +++ b/homeassistant/components/ifttt/translations/zh-Hans.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002", "single_instance_allowed": "\u5b9e\u4f8b\u5df2\u914d\u7f6e\uff0c\u4e14\u53ea\u80fd\u5b58\u5728\u5355\u4e2a\u914d\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u9700\u8981\u7f51\u7edc\u8fde\u63a5\u4ee5\u83b7\u53d6\u76f8\u5173\u63a8\u9001\u4fe1\u606f\u3002" }, diff --git a/homeassistant/components/ifttt/translations/zh-Hant.json b/homeassistant/components/ifttt/translations/zh-Hant.json index fe5b80f72f12b..e6c5392a9c6a4 100644 --- a/homeassistant/components/ifttt/translations/zh-Hant.json +++ b/homeassistant/components/ifttt/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index b96769af932a0..5184bc8c1051f 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": ["iglo==1.2.7"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["iglo"] } diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index e80e3a4eeec12..97836e7f1451e 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": ["georss_ign_sismologia_client==0.3"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["georss_ign_sismologia_client"] } diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 4c5c9d7249709..34b65c5b791ba 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -4,114 +4,24 @@ from ihcsdk.ihccontroller import IHCController import voluptuous as vol -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.const import ( - CONF_ID, - CONF_NAME, - CONF_PASSWORD, - CONF_TYPE, - CONF_UNIT_OF_MEASUREMENT, - CONF_URL, - CONF_USERNAME, - TEMP_CELSIUS, -) +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from .auto_setup import autosetup_ihc_products from .const import ( CONF_AUTOSETUP, - CONF_BINARY_SENSOR, - CONF_DIMMABLE, CONF_INFO, - CONF_INVERTING, - CONF_LIGHT, - CONF_NOTE, - CONF_OFF_ID, - CONF_ON_ID, - CONF_POSITION, - CONF_SENSOR, - CONF_SWITCH, DOMAIN, IHC_CONTROLLER, - IHC_PLATFORMS, + IHC_CONTROLLER_INDEX, ) +from .manual_setup import IHC_SCHEMA, get_manual_configuration from .service_functions import setup_service_functions _LOGGER = logging.getLogger(__name__) -IHC_INFO = "info" - - -def validate_name(config): - """Validate the device name.""" - if CONF_NAME in config: - return config - ihcid = config[CONF_ID] - name = f"ihc_{ihcid}" - config[CONF_NAME] = name - return config - - -DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_ID): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_NOTE): cv.string, - vol.Optional(CONF_POSITION): cv.string, - } -) - - -SWITCH_SCHEMA = DEVICE_SCHEMA.extend( - { - vol.Optional(CONF_OFF_ID, default=0): cv.positive_int, - vol.Optional(CONF_ON_ID, default=0): cv.positive_int, - } -) - -BINARY_SENSOR_SCHEMA = DEVICE_SCHEMA.extend( - { - vol.Optional(CONF_INVERTING, default=False): cv.boolean, - vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA, - } -) - -LIGHT_SCHEMA = DEVICE_SCHEMA.extend( - { - vol.Optional(CONF_DIMMABLE, default=False): cv.boolean, - vol.Optional(CONF_OFF_ID, default=0): cv.positive_int, - vol.Optional(CONF_ON_ID, default=0): cv.positive_int, - } -) - -SENSOR_SCHEMA = DEVICE_SCHEMA.extend( - {vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): cv.string} -) - -IHC_SCHEMA = vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, - vol.Optional(CONF_BINARY_SENSOR, default=[]): vol.All( - cv.ensure_list, [vol.All(BINARY_SENSOR_SCHEMA, validate_name)] - ), - vol.Optional(CONF_INFO, default=True): cv.boolean, - vol.Optional(CONF_LIGHT, default=[]): vol.All( - cv.ensure_list, [vol.All(LIGHT_SCHEMA, validate_name)] - ), - vol.Optional(CONF_SENSOR, default=[]): vol.All( - cv.ensure_list, [vol.All(SENSOR_SCHEMA, validate_name)] - ), - vol.Optional(CONF_SWITCH, default=[]): vol.All( - cv.ensure_list, [vol.All(SWITCH_SCHEMA, validate_name)] - ), - } -) CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [IHC_SCHEMA]))}, extra=vol.ALLOW_EXTRA @@ -124,61 +34,38 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: for index, controller_conf in enumerate(conf): if not ihc_setup(hass, config, controller_conf, index): return False - return True -def ihc_setup(hass, config, conf, controller_id): +def ihc_setup( + hass: HomeAssistant, + config: ConfigType, + controller_conf: ConfigType, + controller_index: int, +) -> bool: """Set up the IHC integration.""" - url = conf[CONF_URL] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] + url = controller_conf[CONF_URL] + username = controller_conf[CONF_USERNAME] + password = controller_conf[CONF_PASSWORD] ihc_controller = IHCController(url, username, password) if not ihc_controller.authenticate(): _LOGGER.error("Unable to authenticate on IHC controller") return False - - if conf[CONF_AUTOSETUP] and not autosetup_ihc_products( + controller_id: str = ihc_controller.client.get_system_info()["serial_number"] + # Store controller configuration + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][controller_id] = { + IHC_CONTROLLER: ihc_controller, + CONF_INFO: controller_conf[CONF_INFO], + IHC_CONTROLLER_INDEX: controller_index, + } + if controller_conf[CONF_AUTOSETUP] and not autosetup_ihc_products( hass, config, ihc_controller, controller_id ): return False - # Manual configuration - get_manual_configuration(hass, config, conf, ihc_controller, controller_id) - # Store controller configuration - ihc_key = f"ihc{controller_id}" - hass.data[ihc_key] = {IHC_CONTROLLER: ihc_controller, IHC_INFO: conf[CONF_INFO]} + get_manual_configuration(hass, config, controller_conf, controller_id) # We only want to register the service functions once for the first controller - if controller_id == 0: + if controller_index == 0: setup_service_functions(hass) return True - - -def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): - """Get manual configuration for IHC devices.""" - for platform in IHC_PLATFORMS: - discovery_info = {} - if platform in conf: - platform_setup = conf.get(platform) - for sensor_cfg in platform_setup: - name = sensor_cfg[CONF_NAME] - device = { - "ihc_id": sensor_cfg[CONF_ID], - "ctrl_id": controller_id, - "product": { - "name": name, - "note": sensor_cfg.get(CONF_NOTE) or "", - "position": sensor_cfg.get(CONF_POSITION) or "", - }, - "product_cfg": { - "type": sensor_cfg.get(CONF_TYPE), - "inverting": sensor_cfg.get(CONF_INVERTING), - "off_id": sensor_cfg.get(CONF_OFF_ID), - "on_id": sensor_cfg.get(CONF_ON_ID), - "dimmable": sensor_cfg.get(CONF_DIMMABLE), - "unit_of_measurement": sensor_cfg.get(CONF_UNIT_OF_MEASUREMENT), - }, - } - discovery_info[name] = device - if discovery_info: - discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) diff --git a/homeassistant/components/ihc/auto_setup.py b/homeassistant/components/ihc/auto_setup.py index f782cd590c026..ae271108848bb 100644 --- a/homeassistant/components/ihc/auto_setup.py +++ b/homeassistant/components/ihc/auto_setup.py @@ -120,19 +120,25 @@ def get_discovery_info(platform_setup, groups, controller_id): for product_cfg in platform_setup: products = group.findall(product_cfg[CONF_XPATH]) for product in products: + product_id = int(product.attrib["id"].strip("_"), 0) nodes = product.findall(product_cfg[CONF_NODE]) for node in nodes: if "setting" in node.attrib and node.attrib["setting"] == "yes": continue ihc_id = int(node.attrib["id"].strip("_"), 0) name = f"{groupname}_{ihc_id}" + # make the model number look a bit nicer - strip leading _ + model = product.get("product_identifier", "").lstrip("_") device = { "ihc_id": ihc_id, "ctrl_id": controller_id, "product": { + "id": product_id, "name": product.get("name") or "", "note": product.get("note") or "", "position": product.get("position") or "", + "model": model, + "group": groupname, }, "product_cfg": product_cfg, } diff --git a/homeassistant/components/ihc/binary_sensor.py b/homeassistant/components/ihc/binary_sensor.py index 7a981fc1b633c..48035d27a4d14 100644 --- a/homeassistant/components/ihc/binary_sensor.py +++ b/homeassistant/components/ihc/binary_sensor.py @@ -1,14 +1,15 @@ """Support for IHC binary sensors.""" from __future__ import annotations +from ihcsdk.ihccontroller import IHCController + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import IHC_CONTROLLER, IHC_INFO -from .const import CONF_INVERTING +from .const import CONF_INVERTING, DOMAIN, IHC_CONTROLLER from .ihcdevice import IHCDevice @@ -27,16 +28,13 @@ def setup_platform( product_cfg = device["product_cfg"] product = device["product"] # Find controller that corresponds with device id - ctrl_id = device["ctrl_id"] - ihc_key = f"ihc{ctrl_id}" - info = hass.data[ihc_key][IHC_INFO] - ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] - + controller_id = device["ctrl_id"] + ihc_controller: IHCController = hass.data[DOMAIN][controller_id][IHC_CONTROLLER] sensor = IHCBinarySensor( ihc_controller, + controller_id, name, ihc_id, - info, product_cfg.get(CONF_TYPE), product_cfg[CONF_INVERTING], product, @@ -54,16 +52,16 @@ class IHCBinarySensor(IHCDevice, BinarySensorEntity): def __init__( self, - ihc_controller, - name, + ihc_controller: IHCController, + controller_id: str, + name: str, ihc_id: int, - info: bool, sensor_type: str, inverting: bool, product=None, ) -> None: """Initialize the IHC binary sensor.""" - super().__init__(ihc_controller, name, ihc_id, info, product) + super().__init__(ihc_controller, controller_id, name, ihc_id, product) self._state = None self._sensor_type = sensor_type self.inverting = inverting diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py index 2f3651c7bb796..c86e77870c8ee 100644 --- a/homeassistant/components/ihc/const.py +++ b/homeassistant/components/ihc/const.py @@ -25,6 +25,7 @@ DOMAIN = "ihc" IHC_CONTROLLER = "controller" +IHC_CONTROLLER_INDEX = "controller_index" IHC_PLATFORMS = ( Platform.BINARY_SENSOR, Platform.LIGHT, diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index e351d2f38eaff..31887c5139757 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,6 +1,14 @@ """Implementation of a base class for all IHC devices.""" +import logging + +from ihcsdk.ihccontroller import IHCController + from homeassistant.helpers.entity import Entity +from .const import CONF_INFO, DOMAIN + +_LOGGER = logging.getLogger(__name__) + class IHCDevice(Entity): """Base class for all IHC devices. @@ -11,17 +19,33 @@ class IHCDevice(Entity): """ def __init__( - self, ihc_controller, name, ihc_id: int, info: bool, product=None + self, + ihc_controller: IHCController, + controller_id: str, + name: str, + ihc_id: int, + product=None, ) -> None: """Initialize IHC attributes.""" self.ihc_controller = ihc_controller self._name = name self.ihc_id = ihc_id - self.info = info + self.controller_id = controller_id + self.device_id = None + self.suggested_area = None if product: self.ihc_name = product["name"] self.ihc_note = product["note"] self.ihc_position = product["position"] + self.suggested_area = product["group"] if "group" in product else None + if "id" in product: + product_id = product["id"] + self.device_id = f"{controller_id}_{product_id }" + # this will name the device the same way as the IHC visual application: Product name + position + self.device_name = product["name"] + if self.ihc_position: + self.device_name += f" ({self.ihc_position})" + self.device_model = product["model"] else: self.ihc_name = "" self.ihc_note = "" @@ -29,6 +53,7 @@ def __init__( async def async_added_to_hass(self): """Add callback for IHC changes.""" + _LOGGER.debug("Adding IHC entity notify event: %s", self.ihc_id) self.ihc_controller.add_notify_event(self.ihc_id, self.on_ihc_change, True) @property @@ -41,17 +66,26 @@ def name(self): """Return the device name.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self.controller_id}-{self.ihc_id}" + @property def extra_state_attributes(self): """Return the state attributes.""" - if not self.info: + if not self.hass.data[DOMAIN][self.controller_id][CONF_INFO]: return {} - return { + attributes = { "ihc_id": self.ihc_id, "ihc_name": self.ihc_name, "ihc_note": self.ihc_note, "ihc_position": self.ihc_position, } + if len(self.hass.data[DOMAIN]) > 1: + # We only want to show the controller id if we have more than one + attributes["ihc_controller"] = self.controller_id + return attributes def on_ihc_change(self, ihc_id, value): """Handle IHC resource change. diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index b6269865072c6..b86f9fb3c8aa5 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,6 +1,8 @@ """Support for IHC lights.""" from __future__ import annotations +from ihcsdk.ihccontroller import IHCController + from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, @@ -10,8 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import IHC_CONTROLLER, IHC_INFO -from .const import CONF_DIMMABLE, CONF_OFF_ID, CONF_ON_ID +from .const import CONF_DIMMABLE, CONF_OFF_ID, CONF_ON_ID, DOMAIN, IHC_CONTROLLER from .ihcdevice import IHCDevice from .util import async_pulse, async_set_bool, async_set_int @@ -31,15 +32,20 @@ def setup_platform( product_cfg = device["product_cfg"] product = device["product"] # Find controller that corresponds with device id - ctrl_id = device["ctrl_id"] - ihc_key = f"ihc{ctrl_id}" - info = hass.data[ihc_key][IHC_INFO] - ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] + controller_id = device["ctrl_id"] + ihc_controller: IHCController = hass.data[DOMAIN][controller_id][IHC_CONTROLLER] ihc_off_id = product_cfg.get(CONF_OFF_ID) ihc_on_id = product_cfg.get(CONF_ON_ID) dimmable = product_cfg[CONF_DIMMABLE] light = IhcLight( - ihc_controller, name, ihc_id, ihc_off_id, ihc_on_id, info, dimmable, product + ihc_controller, + controller_id, + name, + ihc_id, + ihc_off_id, + ihc_on_id, + dimmable, + product, ) devices.append(light) add_entities(devices) @@ -55,17 +61,17 @@ class IhcLight(IHCDevice, LightEntity): def __init__( self, - ihc_controller, - name, + ihc_controller: IHCController, + controller_id: str, + name: str, ihc_id: int, ihc_off_id: int, ihc_on_id: int, - info: bool, dimmable=False, product=None, ) -> None: """Initialize the light.""" - super().__init__(ihc_controller, name, ihc_id, info, product) + super().__init__(ihc_controller, controller_id, name, ihc_id, product) self._ihc_off_id = ihc_off_id self._ihc_on_id = ihc_on_id self._brightness = 0 diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index d6b90f13f8a04..e899a794e070f 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": ["defusedxml==0.7.1", "ihcsdk==2.7.6"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["ihcsdk"] } diff --git a/homeassistant/components/ihc/manual_setup.py b/homeassistant/components/ihc/manual_setup.py new file mode 100644 index 0000000000000..297997281c6e3 --- /dev/null +++ b/homeassistant/components/ihc/manual_setup.py @@ -0,0 +1,142 @@ +"""Handle manual setup of ihc resources as entities in Home Assistant.""" +import logging + +import voluptuous as vol + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.const import ( + CONF_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + CONF_URL, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_AUTOSETUP, + CONF_BINARY_SENSOR, + CONF_DIMMABLE, + CONF_INFO, + CONF_INVERTING, + CONF_LIGHT, + CONF_NOTE, + CONF_OFF_ID, + CONF_ON_ID, + CONF_POSITION, + CONF_SENSOR, + CONF_SWITCH, + DOMAIN, + IHC_PLATFORMS, +) + +_LOGGER = logging.getLogger(__name__) + + +def validate_name(config): + """Validate the device name.""" + if CONF_NAME in config: + return config + ihcid = config[CONF_ID] + name = f"ihc_{ihcid}" + config[CONF_NAME] = name + return config + + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID): cv.positive_int, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NOTE): cv.string, + vol.Optional(CONF_POSITION): cv.string, + } +) + +SWITCH_SCHEMA = DEVICE_SCHEMA.extend( + { + vol.Optional(CONF_OFF_ID, default=0): cv.positive_int, + vol.Optional(CONF_ON_ID, default=0): cv.positive_int, + } +) + +BINARY_SENSOR_SCHEMA = DEVICE_SCHEMA.extend( + { + vol.Optional(CONF_INVERTING, default=False): cv.boolean, + vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + } +) + +LIGHT_SCHEMA = DEVICE_SCHEMA.extend( + { + vol.Optional(CONF_DIMMABLE, default=False): cv.boolean, + vol.Optional(CONF_OFF_ID, default=0): cv.positive_int, + vol.Optional(CONF_ON_ID, default=0): cv.positive_int, + } +) + +SENSOR_SCHEMA = DEVICE_SCHEMA.extend( + {vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): cv.string} +) + +IHC_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, + vol.Optional(CONF_BINARY_SENSOR, default=[]): vol.All( + cv.ensure_list, [vol.All(BINARY_SENSOR_SCHEMA, validate_name)] + ), + vol.Optional(CONF_INFO, default=True): cv.boolean, + vol.Optional(CONF_LIGHT, default=[]): vol.All( + cv.ensure_list, [vol.All(LIGHT_SCHEMA, validate_name)] + ), + vol.Optional(CONF_SENSOR, default=[]): vol.All( + cv.ensure_list, [vol.All(SENSOR_SCHEMA, validate_name)] + ), + vol.Optional(CONF_SWITCH, default=[]): vol.All( + cv.ensure_list, [vol.All(SWITCH_SCHEMA, validate_name)] + ), + } +) + + +def get_manual_configuration( + hass: HomeAssistant, + config: ConfigType, + controller_conf: ConfigType, + controller_id: str, +) -> None: + """Get manual configuration for IHC devices.""" + for platform in IHC_PLATFORMS: + discovery_info = {} + if platform in controller_conf: + platform_setup = controller_conf.get(platform, {}) + for sensor_cfg in platform_setup: + name = sensor_cfg[CONF_NAME] + device = { + "ihc_id": sensor_cfg[CONF_ID], + "ctrl_id": controller_id, + "product": { + "name": name, + "note": sensor_cfg.get(CONF_NOTE) or "", + "position": sensor_cfg.get(CONF_POSITION) or "", + }, + "product_cfg": { + "type": sensor_cfg.get(CONF_TYPE), + "inverting": sensor_cfg.get(CONF_INVERTING), + "off_id": sensor_cfg.get(CONF_OFF_ID), + "on_id": sensor_cfg.get(CONF_ON_ID), + "dimmable": sensor_cfg.get(CONF_DIMMABLE), + "unit_of_measurement": sensor_cfg.get(CONF_UNIT_OF_MEASUREMENT), + }, + } + discovery_info[name] = device + if discovery_info: + discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index d9dcab431d04d..d3c38687caa76 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,6 +1,8 @@ """Support for IHC sensors.""" from __future__ import annotations +from ihcsdk.ihccontroller import IHCController + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -8,7 +10,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_system import TEMPERATURE_UNITS -from . import IHC_CONTROLLER, IHC_INFO +from .const import DOMAIN, IHC_CONTROLLER from .ihcdevice import IHCDevice @@ -27,12 +29,10 @@ def setup_platform( product_cfg = device["product_cfg"] product = device["product"] # Find controller that corresponds with device id - ctrl_id = device["ctrl_id"] - ihc_key = f"ihc{ctrl_id}" - info = hass.data[ihc_key][IHC_INFO] - ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] + controller_id = device["ctrl_id"] + ihc_controller: IHCController = hass.data[DOMAIN][controller_id][IHC_CONTROLLER] unit = product_cfg[CONF_UNIT_OF_MEASUREMENT] - sensor = IHCSensor(ihc_controller, name, ihc_id, info, unit, product) + sensor = IHCSensor(ihc_controller, controller_id, name, ihc_id, unit, product) devices.append(sensor) add_entities(devices) @@ -41,10 +41,16 @@ class IHCSensor(IHCDevice, SensorEntity): """Implementation of the IHC sensor.""" def __init__( - self, ihc_controller, name, ihc_id: int, info: bool, unit, product=None + self, + ihc_controller: IHCController, + controller_id: str, + name: str, + ihc_id: int, + unit: str, + product=None, ) -> None: """Initialize the IHC sensor.""" - super().__init__(ihc_controller, name, ihc_id, info, product) + super().__init__(ihc_controller, controller_id, name, ihc_id, product) self._state = None self._unit_of_measurement = unit diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/service_functions.py index 6136c8ccd4674..3d7008ee38b1b 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/service_functions.py @@ -1,6 +1,4 @@ """Support for IHC devices.""" -import logging - import voluptuous as vol from homeassistant.core import HomeAssistant @@ -12,6 +10,7 @@ ATTR_VALUE, DOMAIN, IHC_CONTROLLER, + IHC_CONTROLLER_INDEX, SERVICE_PULSE, SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_FLOAT, @@ -19,9 +18,6 @@ ) from .util import async_pulse, async_set_bool, async_set_float, async_set_int -_LOGGER = logging.getLogger(__name__) - - SET_RUNTIME_VALUE_BOOL_SCHEMA = vol.Schema( { vol.Required(ATTR_IHC_ID): cv.positive_int, @@ -54,13 +50,17 @@ ) -def setup_service_functions(hass: HomeAssistant): +def setup_service_functions(hass: HomeAssistant) -> None: """Set up the IHC service functions.""" def _get_controller(call): - controller_id = call.data[ATTR_CONTROLLER_ID] - ihc_key = f"ihc{controller_id}" - return hass.data[ihc_key][IHC_CONTROLLER] + controller_index = call.data[ATTR_CONTROLLER_ID] + for controller_id in hass.data[DOMAIN]: + controller_conf = hass.data[DOMAIN][controller_id] + if controller_conf[IHC_CONTROLLER_INDEX] == controller_index: + return controller_conf[IHC_CONTROLLER] + # if not found the controller_index is ouf of range + raise ValueError("The controller index is out of range") async def async_set_runtime_value_bool(call): """Set a IHC runtime bool value service function.""" diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index fb1e2f1642d3b..e33d3b6bb5eae 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,13 +1,14 @@ """Support for IHC switches.""" from __future__ import annotations +from ihcsdk.ihccontroller import IHCController + 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 IHC_CONTROLLER, IHC_INFO -from .const import CONF_OFF_ID, CONF_ON_ID +from .const import CONF_OFF_ID, CONF_ON_ID, DOMAIN, IHC_CONTROLLER from .ihcdevice import IHCDevice from .util import async_pulse, async_set_bool @@ -27,15 +28,13 @@ def setup_platform( product_cfg = device["product_cfg"] product = device["product"] # Find controller that corresponds with device id - ctrl_id = device["ctrl_id"] - ihc_key = f"ihc{ctrl_id}" - info = hass.data[ihc_key][IHC_INFO] - ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] + controller_id = device["ctrl_id"] + ihc_controller: IHCController = hass.data[DOMAIN][controller_id][IHC_CONTROLLER] ihc_off_id = product_cfg.get(CONF_OFF_ID) ihc_on_id = product_cfg.get(CONF_ON_ID) switch = IHCSwitch( - ihc_controller, name, ihc_id, ihc_off_id, ihc_on_id, info, product + ihc_controller, controller_id, name, ihc_id, ihc_off_id, ihc_on_id, product ) devices.append(switch) add_entities(devices) @@ -46,16 +45,16 @@ class IHCSwitch(IHCDevice, SwitchEntity): def __init__( self, - ihc_controller, + ihc_controller: IHCController, + controller_id: str, name: str, ihc_id: int, ihc_off_id: int, ihc_on_id: int, - info: bool, product=None, ) -> None: """Initialize the IHC switch.""" - super().__init__(ihc_controller, name, ihc_id, product) + super().__init__(ihc_controller, controller_id, name, ihc_id, product) self._ihc_off_id = ihc_off_id self._ihc_on_id = ihc_on_id self._state = False diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index c18234597455d..655590005bf0d 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==0.9.0"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aioimaplib"] } diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 7e8a00aee72a4..11946e6238deb 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": ["incomfort-client==0.4.4"], "codeowners": ["@zxdavb"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["incomfortclient"] } diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index 77f76745ba494..f3b0b66df5479 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -72,7 +72,7 @@ EVENT_NEW_STATE = "new_state" DOMAIN = "influxdb" API_VERSION_2 = "2" -TIMEOUT = 5 +TIMEOUT = 10 # seconds RETRY_DELAY = 20 QUEUE_BACKLOG_SECONDS = 30 RETRY_INTERVAL = 60 # seconds diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 2c537c7b35a4e..df2feab5146c0 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": ["influxdb==5.3.1", "influxdb-client==1.24.0"], "codeowners": ["@fabaff", "@mdegat01"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["influxdb", "influxdb_client"] } diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 7404ceb4c7f1c..f9535292e69c4 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_API_VERSION, CONF_NAME, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, @@ -109,6 +110,7 @@ def validate_query_format_for_version(conf: dict) -> dict: _QUERY_SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } @@ -198,6 +200,7 @@ def __init__(self, hass, influx, query): self._value_template = None self._state = None self._hass = hass + self._attr_unique_id = query.get(CONF_UNIQUE_ID) if query[CONF_LANGUAGE] == LANGUAGE_FLUX: query_clause = query.get(CONF_QUERY) diff --git a/homeassistant/components/input_boolean/translations/ja.json b/homeassistant/components/input_boolean/translations/ja.json index 5af782e4e4663..10f2d6d70fada 100644 --- a/homeassistant/components/input_boolean/translations/ja.json +++ b/homeassistant/components/input_boolean/translations/ja.json @@ -5,5 +5,5 @@ "on": "\u30aa\u30f3" } }, - "title": "\u771f\u507d\u5024\u5165\u529b(booleans)" + "title": "\u771f\u507d\u5024\u5165\u529b(Booleans)" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/ja.json b/homeassistant/components/input_datetime/translations/ja.json index aef276095683c..432f4405c03d1 100644 --- a/homeassistant/components/input_datetime/translations/ja.json +++ b/homeassistant/components/input_datetime/translations/ja.json @@ -1,3 +1,3 @@ { - "title": "\u65e5\u6642\u3092\u5165\u529b" + "title": "\u65e5\u6642\u5165\u529b" } \ No newline at end of file diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 4f974d2c18269..ae5fc9d251e74 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -16,6 +16,7 @@ SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -42,21 +43,48 @@ SERVICE_SET_OPTIONS = "set_options" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 +STORAGE_VERSION_MINOR = 2 + + +def _unique(options: Any) -> Any: + try: + return vol.Unique()(options) + except vol.Invalid as exc: + raise HomeAssistantError("Duplicate options are not allowed") from exc + CREATE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), - vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Required(CONF_OPTIONS): vol.All( + cv.ensure_list, vol.Length(min=1), _unique, [cv.string] + ), vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, } UPDATE_FIELDS = { vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_OPTIONS): vol.All( + cv.ensure_list, vol.Length(min=1), _unique, [cv.string] + ), vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, } +def _remove_duplicates(options: list[str], name: str | None) -> list[str]: + """Remove duplicated options.""" + unique_options = list(dict.fromkeys(options)) + # This check was added in 2022.3 + # Reject YAML configured input_select with duplicates from 2022.6 + if len(unique_options) != len(options): + _LOGGER.warning( + "Input select '%s' with options %s had duplicated options, the duplicates have been removed", + name or "", + options, + ) + return unique_options + + def _cv_input_select(cfg: dict[str, Any]) -> dict[str, Any]: """Configure validation helper for input select (voluptuous).""" options = cfg[CONF_OPTIONS] @@ -65,6 +93,7 @@ def _cv_input_select(cfg: dict[str, Any]) -> dict[str, Any]: raise vol.Invalid( f"initial state {initial} is not part of the options: {','.join(options)}" ) + cfg[CONF_OPTIONS] = _remove_duplicates(options, cfg.get(CONF_NAME)) return cfg @@ -89,6 +118,23 @@ def _cv_input_select(cfg: dict[str, Any]) -> dict[str, Any]: RELOAD_SERVICE_SCHEMA = vol.Schema({}) +class InputSelectStore(Store): + """Store entity registry data.""" + + async def _async_migrate_func( + self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any] + ) -> dict[str, Any]: + """Migrate to the new version.""" + if old_major_version == 1: + if old_minor_version < 2: + for item in old_data["items"]: + options = item[ATTR_OPTIONS] + item[ATTR_OPTIONS] = _remove_duplicates( + options, item.get(CONF_NAME) + ) + return old_data + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -102,7 +148,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) storage_collection = InputSelectStorageCollection( - Store(hass, STORAGE_VERSION, STORAGE_KEY), + InputSelectStore( + hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR + ), logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) @@ -301,6 +349,10 @@ def async_previous(self, cycle: bool) -> None: async def async_set_options(self, options: list[str]) -> None: """Set options.""" + unique_options = list(dict.fromkeys(options)) + if len(unique_options) != len(options): + raise HomeAssistantError(f"Duplicated options: {options}") + self._attr_options = options if self.current_option not in self.options: diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 5a8bd4651c56d..8ba16391d7e49 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION @@ -80,8 +79,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 9b32bc400438c..6866e0523684d 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -110,4 +110,4 @@ def get_device_platforms(device): def get_platform_groups(device, domain) -> dict: """Return the platforms that a device belongs in.""" - return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore + return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) # type: ignore[attr-defined] diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 3b0cdee1cc3d7..7abff39113ba5 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -9,5 +9,9 @@ "@teharris1" ], "config_flow": true, - "iot_class": "local_push" -} + "iot_class": "local_push", + "loggers": [ + "pyinsteon", + "pypubsub" + ] +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json index f35d5bad3434f..f26c9cba54c52 100644 --- a/homeassistant/components/insteon/translations/el.json +++ b/homeassistant/components/insteon/translations/el.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\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." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "select_single": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae." + }, "step": { "hubv1": { "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "port": "\u0398\u03cd\u03c1\u03b1" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Insteon Hub Version 1 (\u03c0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf 2014).", @@ -12,11 +21,19 @@ "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", - "port": "\u0398\u03cd\u03c1\u03b1" + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Insteon Hub Version 2.", "title": "Insteon Hub Version 2" }, + "plm": { + "data": { + "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc Insteon PowerLink (PLM).", + "title": "Insteon PLM" + }, "user": { "data": { "modem_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc." @@ -27,8 +44,56 @@ } }, "options": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "input_error": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c3\u03b1\u03c2.", + "select_single": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae." + }, "step": { + "add_override": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0.\u03c7. 1a2b3c)", + "cat": "\u039a\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0.\u03c7. 0x10)", + "subcat": "\u03a5\u03c0\u03bf\u03ba\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0.\u03c7. 0x0a)" + }, + "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "\u039f\u03b9\u03ba\u03b9\u03b1\u03ba\u03cc\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 (a - p)", + "platform": "\u03a0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1", + "steps": "\u0392\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c1\u03bf\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 (\u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd, \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae 22)", + "unitcode": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 (1 - 16)" + }, + "description": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Insteon Hub.", + "title": "Insteon" + }, + "change_hub_config": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0391\u03bb\u03bb\u03ac\u03be\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 Insteon Hub. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2. \u0391\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03af\u03b4\u03b9\u03bf\u03c5 \u03c4\u03bf\u03c5 Hub. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf Hub \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 Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "add_x10": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae X10.", + "change_hub_config": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Hub.", + "remove_override": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "remove_x10": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae X10." + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", + "title": "Insteon" + }, "remove_override": { + "data": { + "address": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7" + }, "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "title": "Insteon" }, diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json index f888c15874bfc..409ac9d62834b 100644 --- a/homeassistant/components/insteon/translations/pt-BR.json +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -1,7 +1,51 @@ { + "config": { + "abort": { + "cannot_connect": "Falha ao conectar", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "select_single": "Selecione uma op\u00e7\u00e3o." + }, + "step": { + "hubv1": { + "data": { + "host": "Endere\u00e7o IP", + "port": "Porta" + }, + "description": "Configure o Insteon Hub Vers\u00e3o 1 (anterior a 2014).", + "title": "Insteon Hub Vers\u00e3o 1" + }, + "hubv2": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "description": "Configure o Insteon Hub Vers\u00e3o 2.", + "title": "Insteon Hub Vers\u00e3o 2" + }, + "plm": { + "data": { + "device": "Caminho do Dispositivo USB" + }, + "description": "Configure o modem Insteon PowerLink (PLM).", + "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Tipo de modem." + }, + "description": "Selecione o tipo de modem Insteon.", + "title": "Insteon" + } + } + }, "options": { "error": { - "cannot_connect": "Falha na conex\u00e3o com o modem Insteon, por favor tente novamente.", + "cannot_connect": "Falha ao conectar", "input_error": "Entradas inv\u00e1lidas, por favor, verifique seus valores.", "select_single": "Selecione uma op\u00e7\u00e3o." }, @@ -9,31 +53,54 @@ "add_override": { "data": { "address": "Endere\u00e7o do dispositivo (ou seja, 1a2b3c)", - "cat": "Subcategoria de dispositivo (ou seja, 0x0a)", + "cat": "Subcategoria de dispositivo (ou seja, 0x10)", "subcat": "Subcategoria de dispositivo (ou seja, 0x0a)" }, - "description": "Escolha um dispositivo para sobrescrever" + "description": "Escolha um dispositivo para sobrescrever", + "title": "Insteon" }, "add_x10": { "data": { + "housecode": "C\u00f3digo da casa (a - p)", "platform": "Plataforma", - "steps": "Etapas de dimmer (apenas para dispositivos de lux, padr\u00e3o 22)" + "steps": "Etapas de dimmer (apenas para dispositivos de lux, padr\u00e3o 22)", + "unitcode": "C\u00f3digo de unidade (1 - 16)" }, - "description": "Altere a senha do Insteon Hub." + "description": "Altere a senha do Insteon Hub.", + "title": "Insteon" }, "change_hub_config": { "data": { - "password": "Nova Senha", - "port": "Novo n\u00famero da porta", - "username": "Novo usu\u00e1rio" - } + "host": "Endere\u00e7o IP", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "description": "Altere as informa\u00e7\u00f5es de conex\u00e3o do Hub Insteon. Voc\u00ea deve reiniciar o Home Assistant depois de fazer essa altera\u00e7\u00e3o. Isso n\u00e3o altera a configura\u00e7\u00e3o do pr\u00f3prio Hub. Para alterar a configura\u00e7\u00e3o no Hub, use o aplicativo Hub.", + "title": "Insteon" }, "init": { "data": { - "add_x10": "Adicionar um dispositivo X10" - } + "add_override": "Adicione uma substitui\u00e7\u00e3o de dispositivo.", + "add_x10": "Adicionar um dispositivo X10", + "change_hub_config": "Altere a configura\u00e7\u00e3o do Hub.", + "remove_override": "Remova uma substitui\u00e7\u00e3o de dispositivo.", + "remove_x10": "Remova um dispositivo X10." + }, + "description": "Selecione uma op\u00e7\u00e3o para configurar.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Selecione um endere\u00e7o de dispositivo para remover" + }, + "description": "Remover uma substitui\u00e7\u00e3o de dispositivo", + "title": "Insteon" }, "remove_x10": { + "data": { + "address": "Selecione um endere\u00e7o de dispositivo para remover" + }, "description": "Remover um dispositivo X10", "title": "Insteon" } diff --git a/homeassistant/components/insteon/translations/sk.json b/homeassistant/components/insteon/translations/sk.json new file mode 100644 index 0000000000000..c563a509f0717 --- /dev/null +++ b/homeassistant/components/insteon/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "hubv1": { + "data": { + "port": "Port" + } + }, + "hubv2": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "change_hub_config": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json index 302d8c3676a00..747e3a301767c 100644 --- a/homeassistant/components/insteon/translations/uk.json +++ b/homeassistant/components/insteon/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index dd69e0ec7c4d2..cf090f974f75b 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index c95c96c505bb6..7a6248254d82d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -49,7 +49,7 @@ INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] # SI Metric prefixes -UNIT_PREFIXES = {None: 1, "k": 10 ** 3, "M": 10 ** 6, "G": 10 ** 9, "T": 10 ** 12} +UNIT_PREFIXES = {None: 1, "k": 10**3, "M": 10**6, "G": 10**9, "T": 10**12} # SI Time prefixes UNIT_TIME = { diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index 62082bb9ab40f..747dcaa58bef3 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -13,10 +13,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IntellifireDataUpdateCoordinator from .const import DOMAIN +from .entity import IntellifireEntity @dataclass @@ -75,28 +75,11 @@ async def async_setup_entry( ) -class IntellifireBinarySensor(CoordinatorEntity, BinarySensorEntity): - """A semi-generic wrapper around Binary Sensor entities for IntelliFire.""" +class IntellifireBinarySensor(IntellifireEntity, BinarySensorEntity): + """Extends IntellifireEntity with Binary Sensor specific logic.""" - # Define types - coordinator: IntellifireDataUpdateCoordinator entity_description: IntellifireBinarySensorEntityDescription - def __init__( - self, - coordinator: IntellifireDataUpdateCoordinator, - description: IntellifireBinarySensorEntityDescription, - ) -> None: - """Class initializer.""" - super().__init__(coordinator=coordinator) - self.entity_description = description - - # Set the Display name the User will see - self._attr_name = f"Fireplace {description.name}" - self._attr_unique_id = f"{description.key}_{coordinator.api.data.serial}" - # Configure the Device Info - self._attr_device_info = self.coordinator.device_info - @property def is_on(self) -> bool: """Use this to get the correct value.""" diff --git a/homeassistant/components/intellifire/entity.py b/homeassistant/components/intellifire/entity.py new file mode 100644 index 0000000000000..eeb5e7b51bd9a --- /dev/null +++ b/homeassistant/components/intellifire/entity.py @@ -0,0 +1,28 @@ +"""Platform for shared base classes for sensors.""" +from __future__ import annotations + +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import IntellifireDataUpdateCoordinator + + +class IntellifireEntity(CoordinatorEntity): + """Define a generic class for Intellifire entities.""" + + coordinator: IntellifireDataUpdateCoordinator + _attr_attribution = "Data provided by unpublished Intellifire API" + + def __init__( + self, + coordinator: IntellifireDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Class initializer.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + # Set the Display name the User will see + self._attr_name = f"Fireplace {description.name}" + self._attr_unique_id = f"{description.key}_{coordinator.api.data.serial}" + # Configure the Device Info + self._attr_device_info = self.coordinator.device_info diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 965bb6f32dbaf..03862a6ef5ff3 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,8 +3,9 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==0.6"], + "requirements": ["intellifire4py==0.9.9"], "dependencies": [], "codeowners": ["@jeeftor"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["intellifire4py"] } diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index 991e4e69e8c1e..b61ea4437287b 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -16,48 +16,13 @@ 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 homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow -from . import IntellifireDataUpdateCoordinator from .const import DOMAIN - - -class IntellifireSensor(CoordinatorEntity, SensorEntity): - """Define a generic class for Sensors.""" - - # Define types - coordinator: IntellifireDataUpdateCoordinator - entity_description: IntellifireSensorEntityDescription - _attr_attribution = "Data provided by unpublished Intellifire API" - - def __init__( - self, - coordinator: IntellifireDataUpdateCoordinator, - description: IntellifireSensorEntityDescription, - ) -> None: - """Init the sensor.""" - super().__init__(coordinator=coordinator) - self.entity_description = description - - # Set the Display name the User will see - self._attr_name = f"Fireplace {description.name}" - self._attr_unique_id = f"{description.key}_{coordinator.api.data.serial}" - # Configure the Device Info - self._attr_device_info = self.coordinator.device_info - - @property - def native_value(self) -> int | str | datetime | None: - """Return the state.""" - return self.entity_description.value_fn(self.coordinator.api.data) - - -def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None: - """Define a sensor that takes into account timezone.""" - if not (seconds_offset := data.timeremaining_s): - return None - return utcnow() + timedelta(seconds=seconds_offset) +from .coordinator import IntellifireDataUpdateCoordinator +from .entity import IntellifireEntity @dataclass @@ -69,21 +34,24 @@ class IntellifireSensorRequiredKeysMixin: @dataclass class IntellifireSensorEntityDescription( - SensorEntityDescription, IntellifireSensorRequiredKeysMixin + SensorEntityDescription, + IntellifireSensorRequiredKeysMixin, ): - """Describes a sensor sensor entity.""" + """Describes a sensor entity.""" -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Define setup entry call.""" +def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None: + """Define a sensor that takes into account timezone.""" + if not (seconds_offset := data.timeremaining_s): + return None + return utcnow() + timedelta(seconds=seconds_offset) - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - IntellifireSensor(coordinator=coordinator, description=description) - for description in INTELLIFIRE_SENSORS - ) + +def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: + """Define a sensor that takes into account a timezone.""" + if not (seconds_offset := data.downtime): + return None + return utcnow() - timedelta(seconds=seconds_offset) INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( @@ -125,4 +93,61 @@ async def async_setup_entry( device_class=SensorDeviceClass.TIMESTAMP, value_fn=_time_remaining_to_timestamp, ), + IntellifireSensorEntityDescription( + key="downtime", + name="Downtime", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=_downtime_to_timestamp, + ), + IntellifireSensorEntityDescription( + key="uptime", + name="Uptime", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime), + ), + IntellifireSensorEntityDescription( + key="connection_quality", + name="Connection Quality", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.connection_quality, + entity_registry_enabled_default=False, + ), + IntellifireSensorEntityDescription( + key="ecm_latency", + name="ECM Latency", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.ecm_latency, + entity_registry_enabled_default=False, + ), + IntellifireSensorEntityDescription( + key="ipv4_address", + name="IP", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.ipv4_address, + ), ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Define setup entry call.""" + + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + IntellifireSensor(coordinator=coordinator, description=description) + for description in INTELLIFIRE_SENSORS + ) + + +class IntellifireSensor(IntellifireEntity, SensorEntity): + """Extends IntellifireEntity with Sensor specific logic.""" + + entity_description: IntellifireSensorEntityDescription + + @property + def native_value(self) -> int | str | datetime | None: + """Return the state.""" + return self.entity_description.value_fn(self.coordinator.api.data) diff --git a/homeassistant/components/intellifire/translations/el.json b/homeassistant/components/intellifire/translations/el.json new file mode 100644 index 0000000000000..c7b88a9b3d853 --- /dev/null +++ b/homeassistant/components/intellifire/translations/el.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/fr.json b/homeassistant/components/intellifire/translations/fr.json index e019c6ac5ef12..88a0aeb68c885 100644 --- a/homeassistant/components/intellifire/translations/fr.json +++ b/homeassistant/components/intellifire/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { + "cannot_connect": "\u00c9chec de connexion", "unknown": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "step": { diff --git a/homeassistant/components/intellifire/translations/pl.json b/homeassistant/components/intellifire/translations/pl.json new file mode 100644 index 0000000000000..d455990b1f032 --- /dev/null +++ b/homeassistant/components/intellifire/translations/pl.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/pt-BR.json b/homeassistant/components/intellifire/translations/pt-BR.json new file mode 100644 index 0000000000000..ff6ede166a9dd --- /dev/null +++ b/homeassistant/components/intellifire/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json new file mode 100644 index 0000000000000..f341a6314eeb0 --- /dev/null +++ b/homeassistant/components/intellifire/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 2deca297f34bd..d14aaf5a68bae 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -12,6 +12,7 @@ CONF_INTENTS = "intents" CONF_SPEECH = "speech" +CONF_REPROMPT = "reprompt" CONF_ACTION = "action" CONF_CARD = "card" @@ -39,6 +40,10 @@ vol.Optional(CONF_TYPE, default="plain"): cv.string, vol.Required(CONF_TEXT): cv.template, }, + vol.Optional(CONF_REPROMPT): { + vol.Optional(CONF_TYPE, default="plain"): cv.string, + vol.Required(CONF_TEXT): cv.template, + }, } } }, @@ -72,6 +77,7 @@ def __init__(self, intent_type, config): async def async_handle(self, intent_obj): """Handle the intent.""" speech = self.config.get(CONF_SPEECH) + reprompt = self.config.get(CONF_REPROMPT) card = self.config.get(CONF_CARD) action = self.config.get(CONF_ACTION) is_async_action = self.config.get(CONF_ASYNC_ACTION) @@ -93,6 +99,12 @@ async def async_handle(self, intent_obj): speech[CONF_TYPE], ) + if reprompt is not None and reprompt[CONF_TEXT].template: + response.async_set_reprompt( + reprompt[CONF_TEXT].async_render(slots, parse_result=False), + reprompt[CONF_TYPE], + ) + if card is not None: response.async_set_card( card[CONF_TITLE].async_render(slots, parse_result=False), diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index 44d4d4ca58249..6b84f735c12d4 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/intesishome", "codeowners": ["@jnimmo"], "requirements": ["pyintesishome==1.7.6"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pyintesishome"] } diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py index fc15cd833bc9e..47959e34074f8 100644 --- a/homeassistant/components/ios/config_flow.py +++ b/homeassistant/components/ios/config_flow.py @@ -3,4 +3,6 @@ from .const import DOMAIN -config_entry_flow.register_discovery_flow(DOMAIN, "Home Assistant iOS", lambda *_: True) +config_entry_flow.register_discovery_flow( + DOMAIN, "Home Assistant iOS", lambda hass: True +) diff --git a/homeassistant/components/ios/translations/el.json b/homeassistant/components/ios/translations/el.json new file mode 100644 index 0000000000000..364238e98a7eb --- /dev/null +++ b/homeassistant/components/ios/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/pt-BR.json b/homeassistant/components/ios/translations/pt-BR.json index fffbfae22494a..369064ba6cb5e 100644 --- a/homeassistant/components/ios/translations/pt-BR.json +++ b/homeassistant/components/ios/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do Home Assistant iOS \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { - "description": "Deseja configurar o componente iOS do Home Assistant?" + "description": "Deseja iniciar a configura\u00e7\u00e3o?" } } } diff --git a/homeassistant/components/ios/translations/sk.json b/homeassistant/components/ios/translations/sk.json new file mode 100644 index 0000000000000..e227301685bbd --- /dev/null +++ b/homeassistant/components/ios/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/uk.json b/homeassistant/components/ios/translations/uk.json index 5f8d69f5f29b8..5ee7dbfde346d 100644 --- a/homeassistant/components/ios/translations/uk.json +++ b/homeassistant/components/ios/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/ios/translations/zh-Hant.json b/homeassistant/components/ios/translations/zh-Hant.json index aceb4ea78d528..649ab1f56e608 100644 --- a/homeassistant/components/ios/translations/zh-Hant.json +++ b/homeassistant/components/ios/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/iotawatt/coordinator.py b/homeassistant/components/iotawatt/coordinator.py index ada9c9fb346ca..6c97fc99169e2 100644 --- a/homeassistant/components/iotawatt/coordinator.py +++ b/homeassistant/components/iotawatt/coordinator.py @@ -10,12 +10,16 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import httpx_client +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONNECTION_ERRORS _LOGGER = logging.getLogger(__name__) +# Matches iotwatt data log interval +REQUEST_REFRESH_DEFAULT_COOLDOWN = 5 + class IotawattUpdater(DataUpdateCoordinator): """Class to manage fetching update data from the IoTaWatt Energy Device.""" @@ -30,6 +34,12 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: logger=_LOGGER, name=entry.title, update_interval=timedelta(seconds=30), + request_refresh_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=REQUEST_REFRESH_DEFAULT_COOLDOWN, + immediate=True, + ), ) self._last_run: datetime | None = None @@ -51,6 +61,7 @@ async def _async_update_data(self): httpx_client.get_async_client(self.hass), self.entry.data.get(CONF_USERNAME), self.entry.data.get(CONF_PASSWORD), + integratedInterval="d", ) try: is_authenticated = await api.connect() diff --git a/homeassistant/components/iotawatt/manifest.json b/homeassistant/components/iotawatt/manifest.json index 42e1e074c8e10..5addb8699947d 100644 --- a/homeassistant/components/iotawatt/manifest.json +++ b/homeassistant/components/iotawatt/manifest.json @@ -10,5 +10,6 @@ "@gtdiehl", "@jyavenard" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["iotawattpy"] } \ No newline at end of file diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 62f6574156687..c3c173f778ec1 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -210,6 +210,12 @@ def _handle_coordinator_update(self) -> None: else: self.hass.async_create_task(self.async_remove()) return + + if (begin := self._sensor_data.getBegin()) and ( + last_reset := dt.parse_datetime(begin) + ): + self._attr_last_reset = last_reset + super()._handle_coordinator_update() @property @@ -219,6 +225,7 @@ def extra_state_attributes(self) -> dict[str, str]: attrs = {"type": data.getType()} if attrs["type"] == "Input": attrs["channel"] = data.getChannel() + return attrs @property diff --git a/homeassistant/components/iotawatt/translations/el.json b/homeassistant/components/iotawatt/translations/el.json index 4499676487351..4a47ab1befa08 100644 --- a/homeassistant/components/iotawatt/translations/el.json +++ b/homeassistant/components/iotawatt/translations/el.json @@ -2,6 +2,7 @@ "config": { "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\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { @@ -11,6 +12,11 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae IoTawatt \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae." + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } } } } diff --git a/homeassistant/components/iotawatt/translations/nb.json b/homeassistant/components/iotawatt/translations/nb.json new file mode 100644 index 0000000000000..b97053efa8581 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/pt-BR.json b/homeassistant/components/iotawatt/translations/pt-BR.json new file mode 100644 index 0000000000000..9fec74f379fdf --- /dev/null +++ b/homeassistant/components/iotawatt/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "auth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "O dispositivo IoTawatt requer autentica\u00e7\u00e3o. Por favor, digite o nome de usu\u00e1rio e senha e clique no bot\u00e3o Enviar." + }, + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/sk.json b/homeassistant/components/iotawatt/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 6cebb34bc6347..463f921f03bc4 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": ["iperf3==0.1.11"], "codeowners": ["@rohankapoorcom"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["iperf3"] } diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 06079bf0b5c48..902a03b6c83b7 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": ["pyipma==2.0.5"], "codeowners": ["@dgomes", "@abmantis"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geopy", "pyipma"] } diff --git a/homeassistant/components/ipma/translations/el.json b/homeassistant/components/ipma/translations/el.json index 932be8297f19d..86b71fc8c92ae 100644 --- a/homeassistant/components/ipma/translations/el.json +++ b/homeassistant/components/ipma/translations/el.json @@ -5,7 +5,14 @@ }, "step": { "user": { - "description": "Instituto Portugu\u00eas do Mar e Atmosfera" + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" } } }, diff --git a/homeassistant/components/ipma/translations/pt-BR.json b/homeassistant/components/ipma/translations/pt-BR.json index f2af40324ebff..b6022ba812493 100644 --- a/homeassistant/components/ipma/translations/pt-BR.json +++ b/homeassistant/components/ipma/translations/pt-BR.json @@ -15,5 +15,10 @@ "title": "Localiza\u00e7\u00e3o" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint da API IPMA acess\u00edvel" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/sk.json b/homeassistant/components/ipma/translations/sk.json new file mode 100644 index 0000000000000..e5a635afe105f --- /dev/null +++ b/homeassistant/components/ipma/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + }, + "title": "Umiestnenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 18bfc3abc5480..39e798f99bff7 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "quality_scale": "platinum", "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["deepmerge", "pyipp"] } diff --git a/homeassistant/components/ipp/translations/el.json b/homeassistant/components/ipp/translations/el.json index 0c3b36ba0afaa..ec29a112b6119 100644 --- a/homeassistant/components/ipp/translations/el.json +++ b/homeassistant/components/ipp/translations/el.json @@ -1,18 +1,34 @@ { "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", "connection_upgrade": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", - "parse_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae." + "ipp_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 IPP.", + "ipp_version_error": "\u0397 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 IPP \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae.", + "parse_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae.", + "unique_id_required": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03ae \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "connection_upgrade": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae SSL/TLS." }, "flow_title": "{name}", "step": { "user": { "data": { - "base_path": "\u03a3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae" - } + "base_path": "\u03a3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\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", + "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" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae \u03c3\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5 \u03b5\u03ba\u03c4\u03cd\u03c0\u03c9\u03c3\u03b7\u03c2 Internet (IPP) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant.", + "title": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae \u03c3\u03b1\u03c2" + }, + "zeroconf_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2" } } } diff --git a/homeassistant/components/ipp/translations/pt-BR.json b/homeassistant/components/ipp/translations/pt-BR.json index 704cc017a9b53..7da66ff568bee 100644 --- a/homeassistant/components/ipp/translations/pt-BR.json +++ b/homeassistant/components/ipp/translations/pt-BR.json @@ -1,27 +1,33 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", "connection_upgrade": "Falha ao conectar \u00e0 impressora devido \u00e0 atualiza\u00e7\u00e3o da conex\u00e3o ser necess\u00e1ria.", "ipp_error": "Erro IPP encontrado.", "ipp_version_error": "Vers\u00e3o IPP n\u00e3o suportada pela impressora.", + "parse_error": "Falha ao analisar a resposta da impressora.", "unique_id_required": "Dispositivo faltando identifica\u00e7\u00e3o \u00fanica necess\u00e1ria para a descoberta." }, "error": { + "cannot_connect": "Falha ao conectar", "connection_upgrade": "Falha ao conectar \u00e0 impressora. Por favor, tente novamente com a op\u00e7\u00e3o SSL/TLS marcada." }, - "flow_title": "Impressora: {name}", + "flow_title": "{name}", "step": { "user": { "data": { "base_path": "Caminho relativo para a impressora", + "host": "Nome do host", "port": "Porta", - "ssl": "A impressora suporta comunica\u00e7\u00e3o via SSL/TLS", - "verify_ssl": "A impressora usa um certificado SSL adequado" + "ssl": "Usar um certificado SSL", + "verify_ssl": "Verifique o certificado SSL" }, "description": "Configure sua impressora via IPP (Internet Printing Protocol) para integrar-se ao Home Assistant.", "title": "Vincule sua impressora" }, "zeroconf_confirm": { + "description": "Deseja configurar {name}?", "title": "Impressora descoberta" } } diff --git a/homeassistant/components/ipp/translations/sk.json b/homeassistant/components/ipp/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/ipp/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index f78ca1e258cd4..dc91ede5461e6 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": ["numpy==1.21.4", "pyiqvia==2021.11.0"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyiqvia"] } diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 46da1aea0aa43..51f2969e9fe46 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -161,7 +161,7 @@ def calculate_trend(indices: list[float]) -> str: """Calculate the "moving average" of a set of indices.""" index_range = np.arange(0, len(indices)) index_array = np.array(indices) - linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore + linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore[no-untyped-call] slope = round(linear_fit[0], 2) if slope > 0: diff --git a/homeassistant/components/iqvia/translations/el.json b/homeassistant/components/iqvia/translations/el.json new file mode 100644 index 0000000000000..4a3045201e278 --- /dev/null +++ b/homeassistant/components/iqvia/translations/el.json @@ -0,0 +1,19 @@ +{ + "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": { + "invalid_zip_code": "\u039f \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2" + }, + "step": { + "user": { + "data": { + "zip_code": "\u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2" + }, + "description": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 \u03ae \u03c4\u03bf\u03c5 \u039a\u03b1\u03bd\u03b1\u03b4\u03ac.", + "title": "IQVIA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/pt-BR.json b/homeassistant/components/iqvia/translations/pt-BR.json index d8ceb8fe93449..a366280ec353e 100644 --- a/homeassistant/components/iqvia/translations/pt-BR.json +++ b/homeassistant/components/iqvia/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "invalid_zip_code": "C\u00f3digo postal inv\u00e1lido" }, diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index 4263d5288ff1e..d6938916c9ad2 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": ["pyirishrail==0.0.2"], "codeowners": ["@ttroy50"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyirishrail"] } diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 5aaa243282b14..c88e26e1c90cc 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -201,7 +201,7 @@ async def async_add_options(self): ) @staticmethod - async def async_options_updated(hass, entry): + async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Triggered by config entry options updates.""" if hass.data[DOMAIN].event_unsub: hass.data[DOMAIN].event_unsub() diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index e72eb0a6da73c..455f3bab675c2 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -5,5 +5,6 @@ "requirements": ["prayer_times_calculator==0.0.5"], "codeowners": ["@engrbm87"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["prayer_times_calculator"] } diff --git a/homeassistant/components/islamic_prayer_times/translations/pt-BR.json b/homeassistant/components/islamic_prayer_times/translations/pt-BR.json new file mode 100644 index 0000000000000..c55992a3e9792 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Voc\u00ea quer configurar tempo de ora\u00e7\u00e3o Isl\u00e2mico?", + "title": "Estabele\u00e7a hor\u00e1rios de ora\u00e7\u00e3o Isl\u00e2mico" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "M\u00e9todo de c\u00e1lculo de ora\u00e7\u00e3o" + } + } + } + }, + "title": "Tempo de ora\u00e7\u00e3o Isl\u00e2mico" +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/uk.json b/homeassistant/components/islamic_prayer_times/translations/uk.json index 9290114899a4e..3774d17802596 100644 --- a/homeassistant/components/islamic_prayer_times/translations/uk.json +++ b/homeassistant/components/islamic_prayer_times/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { "user": { diff --git a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json index ea7a2c4f9b2d2..a77fa8136bb5f 100644 --- a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json +++ b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 51487bdfaf2e4..997c3fff2a3a3 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -1 +1,33 @@ """The iss component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + hass.data.setdefault(DOMAIN, {}) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + 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/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index eab1294ad1023..12a8a7514b2c4 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for International Space Station binary sensor.""" +"""Support for iss binary sensor.""" from __future__ import annotations from datetime import timedelta @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -22,6 +23,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) ATTR_ISS_NEXT_RISE = "next_rise" @@ -46,22 +49,39 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the ISS binary sensor.""" - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return + """Import ISS configuration from yaml.""" + _LOGGER.warning( + "Configuration of the iss platform in YAML is deprecated and will be " + "removed in Home Assistant 2022.5; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + name = entry.title + show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) try: iss_data = IssData(hass.config.latitude, hass.config.longitude) - iss_data.update() + await hass.async_add_executor_job(iss_data.update) except HTTPError as error: _LOGGER.error(error) return - name = config.get(CONF_NAME) - show_on_map = config.get(CONF_SHOW_ON_MAP) - - add_entities([IssBinarySensor(iss_data, name, show_on_map)], True) + async_add_entities([IssBinarySensor(iss_data, name, show_on_map)], True) class IssBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py new file mode 100644 index 0000000000000..dc80126bd140c --- /dev/null +++ b/homeassistant/components/iss/config_flow.py @@ -0,0 +1,80 @@ +"""Config flow to configure iss component.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult + +from .binary_sensor import DEFAULT_NAME +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for iss component.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None) -> FlowResult: + """Handle a flow initialized by the user.""" + # Check if already configured + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # Check if location have been defined. + if not self.hass.config.latitude and not self.hass.config.longitude: + return self.async_abort(reason="latitude_longitude_not_defined") + + if user_input is not None: + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + options={CONF_SHOW_ON_MAP: user_input.get(CONF_SHOW_ON_MAP, False)}, + ) + + return self.async_show_form(step_id="user") + + async def async_step_import(self, conf: dict) -> FlowResult: + """Import a configuration from configuration.yaml.""" + return await self.async_step_user( + user_input={ + CONF_NAME: conf[CONF_NAME], + CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], + } + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options handler for iss.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None) -> FlowResult: + """Manage the options.""" + if user_input is not None: + self.options.update(user_input) + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_SHOW_ON_MAP, + default=self.config_entry.options.get(CONF_SHOW_ON_MAP, False), + ): bool, + } + ), + ) diff --git a/homeassistant/components/iss/const.py b/homeassistant/components/iss/const.py new file mode 100644 index 0000000000000..3d240041b67f8 --- /dev/null +++ b/homeassistant/components/iss/const.py @@ -0,0 +1,3 @@ +"""Constants for iss.""" + +DOMAIN = "iss" diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index be34babeeae5a..bd1aa967f070f 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -1,8 +1,10 @@ { "domain": "iss", + "config_flow": true, "name": "International Space Station (ISS)", "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": ["pyiss==1.0.1"], - "codeowners": [], - "iot_class": "cloud_polling" + "codeowners": ["@DurgNomis-drol"], + "iot_class": "cloud_polling", + "loggers": ["pyiss"] } diff --git a/homeassistant/components/iss/strings.json b/homeassistant/components/iss/strings.json new file mode 100644 index 0000000000000..4a2da5f655604 --- /dev/null +++ b/homeassistant/components/iss/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to configure International Space Station (ISS)?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant." + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show on map" + } + } + } + } + } diff --git a/homeassistant/components/iss/translations/bg.json b/homeassistant/components/iss/translations/bg.json new file mode 100644 index 0000000000000..05945056fb496 --- /dev/null +++ b/homeassistant/components/iss/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430\u0442\u0430 \u0448\u0438\u0440\u0438\u043d\u0430 \u0438 \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430\u0442\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430 \u043d\u0435 \u0441\u0430 \u0434\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0438 \u0432 Home Assistant.", + "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." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u0414\u0430 \u0441\u0435 \u043f\u043e\u043a\u0430\u0436\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430?" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u041c\u0435\u0436\u0434\u0443\u043d\u0430\u0440\u043e\u0434\u043d\u0430\u0442\u0430 \u043a\u043e\u0441\u043c\u0438\u0447\u0435\u0441\u043a\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/ca.json b/homeassistant/components/iss/translations/ca.json new file mode 100644 index 0000000000000..23e4ff4eabdd6 --- /dev/null +++ b/homeassistant/components/iss/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "La latitud i longitud no estan definits a Home Assistant.", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostrar al mapa?" + }, + "description": "Vols configurar Estaci\u00f3 Espacial Internacional (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra al mapa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/cs.json b/homeassistant/components/iss/translations/cs.json new file mode 100644 index 0000000000000..19f5d1e158757 --- /dev/null +++ b/homeassistant/components/iss/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/iss/translations/de.json b/homeassistant/components/iss/translations/de.json new file mode 100644 index 0000000000000..04ae0f6e9d536 --- /dev/null +++ b/homeassistant/components/iss/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Breiten- und L\u00e4ngengrad sind im Home Assistant nicht definiert.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "data": { + "show_on_map": "Auf der Karte anzeigen?" + }, + "description": "M\u00f6chtest du die Internationale Raumstation (ISS) konfigurieren?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Auf Karte anzeigen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/el.json b/homeassistant/components/iss/translations/el.json new file mode 100644 index 0000000000000..b662dbea64c5f --- /dev/null +++ b/homeassistant/components/iss/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "\u03a4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2 \u03b4\u03b5\u03bd \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant.", + "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." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7;" + }, + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u0394\u03b9\u03b5\u03b8\u03bd\u03ae \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03cc \u03a3\u03c4\u03b1\u03b8\u03bc\u03cc;" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/en.json b/homeassistant/components/iss/translations/en.json new file mode 100644 index 0000000000000..56f9bd79e880e --- /dev/null +++ b/homeassistant/components/iss/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Latitude and longitude are not defined in Home Assistant.", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "data": { + "show_on_map": "Show on map?" + }, + "description": "Do you want to configure International Space Station (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show on map" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json new file mode 100644 index 0000000000000..91bbd571ab9ac --- /dev/null +++ b/homeassistant/components/iss/translations/es.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar en el mapa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/et.json b/homeassistant/components/iss/translations/et.json new file mode 100644 index 0000000000000..60385881b19e7 --- /dev/null +++ b/homeassistant/components/iss/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Laius- ja pikkuskraad pole Home Assistandis m\u00e4\u00e4ratud.", + "single_instance_allowed": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "data": { + "show_on_map": "Kas n\u00e4idata kaardil?" + }, + "description": "Kas seadistada rahvusvahelise kosmosejaama (ISS) sidumist?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Kuva kaardil" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/fr.json b/homeassistant/components/iss/translations/fr.json new file mode 100644 index 0000000000000..fd0b2adba422f --- /dev/null +++ b/homeassistant/components/iss/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "La latitude et la longitude ne sont pas d\u00e9finies dans Home Assistant.", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "data": { + "show_on_map": "Afficher sur la carte?" + }, + "description": "Voulez-vous configurer la Station spatiale internationale?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Montrer sur la carte" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/he.json b/homeassistant/components/iss/translations/he.json new file mode 100644 index 0000000000000..eaea05d0779ec --- /dev/null +++ b/homeassistant/components/iss/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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." + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u05d4\u05e6\u05d2 \u05d1\u05de\u05e4\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/hu.json b/homeassistant/components/iss/translations/hu.json new file mode 100644 index 0000000000000..1d75dd9ed3f72 --- /dev/null +++ b/homeassistant/components/iss/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "A f\u00f6ldrajzi sz\u00e9less\u00e9g \u00e9s hossz\u00fas\u00e1g nincs megadva Home Assistantban.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "data": { + "show_on_map": "Megjelenjen a t\u00e9rk\u00e9pen?" + }, + "description": "Szeretn\u00e9 konfigur\u00e1lni a Nemzetk\u00f6zi \u0170r\u00e1llom\u00e1st?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Megjelen\u00edt\u00e9s a t\u00e9rk\u00e9pen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/id.json b/homeassistant/components/iss/translations/id.json new file mode 100644 index 0000000000000..c53287164eec7 --- /dev/null +++ b/homeassistant/components/iss/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Lintang dan bujur tidak didefinisikan dalam Home Assistant.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "data": { + "show_on_map": "Tampilkan di peta?" + }, + "description": "Ingin mengonfigurasi Stasiun Luar Angkasa Internasional (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Tampilkan di peta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/it.json b/homeassistant/components/iss/translations/it.json new file mode 100644 index 0000000000000..b3ec1329eaeed --- /dev/null +++ b/homeassistant/components/iss/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Latitudine e longitudine non sono definite in Home Assistant.", + "single_instance_allowed": "Gi\u00e0 configurato. Solo una configurazione \u00e8 ammessa." + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostrare sulla mappa?" + }, + "description": "Vuoi configurare la Stazione Spaziale Internazionale (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra sulla mappa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json new file mode 100644 index 0000000000000..40178b203f937 --- /dev/null +++ b/homeassistant/components/iss/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "data": { + "show_on_map": "\u5730\u56f3\u306b\u8868\u793a\u3057\u307e\u3059\u304b\uff1f" + }, + "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u5730\u56f3\u306b\u8868\u793a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/nb.json b/homeassistant/components/iss/translations/nb.json new file mode 100644 index 0000000000000..686be80ba764c --- /dev/null +++ b/homeassistant/components/iss/translations/nb.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Lengde- og breddegrad er ikke definert i Home Assistant.", + "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n enkelt konfigurasjon er mulig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/nl.json b/homeassistant/components/iss/translations/nl.json new file mode 100644 index 0000000000000..c3e7ade05d995 --- /dev/null +++ b/homeassistant/components/iss/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Breedte- en lengtegraad zijn niet gedefinieerd in Home Assistant.", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "data": { + "show_on_map": "Op kaart tonen?" + }, + "description": "Wilt u het International Space Station configureren?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Toon op kaart" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/no.json b/homeassistant/components/iss/translations/no.json new file mode 100644 index 0000000000000..c204f5a90125e --- /dev/null +++ b/homeassistant/components/iss/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Bredde- og lengdegrad er ikke definert i Home Assistant.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "data": { + "show_on_map": "Vis p\u00e5 kart?" + }, + "description": "Vil du konfigurere den internasjonale romstasjonen (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Vis p\u00e5 kart" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/pl.json b/homeassistant/components/iss/translations/pl.json new file mode 100644 index 0000000000000..2cdf8ef48631c --- /dev/null +++ b/homeassistant/components/iss/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzna nie s\u0105 zdefiniowane w Home Assistant.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "data": { + "show_on_map": "Pokaza\u0107 na mapie?" + }, + "description": "Czy chcesz skonfigurowa\u0107 Mi\u0119dzynarodow\u0105 Stacj\u0119 Kosmiczn\u0105 (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Poka\u017c na mapie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/pt-BR.json b/homeassistant/components/iss/translations/pt-BR.json new file mode 100644 index 0000000000000..5e34b2eec1bf4 --- /dev/null +++ b/homeassistant/components/iss/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Latitude e longitude n\u00e3o est\u00e3o definidos no Home Assistant.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostrar no mapa?" + }, + "description": "Deseja configurar a Esta\u00e7\u00e3o Espacial Internacional (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar no mapa?" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/ru.json b/homeassistant/components/iss/translations/ru.json new file mode 100644 index 0000000000000..64604c1f460d0 --- /dev/null +++ b/homeassistant/components/iss/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u0432 Home Assistant.", + "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." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + }, + "description": "\u041d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 International Space Station (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/tr.json b/homeassistant/components/iss/translations/tr.json new file mode 100644 index 0000000000000..3cb92db229c27 --- /dev/null +++ b/homeassistant/components/iss/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Enlem ve boylam Home Assistant'ta tan\u0131ml\u0131 de\u011fil.", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "data": { + "show_on_map": "Haritada g\u00f6sterilsin mi?" + }, + "description": "Uluslararas\u0131 Uzay \u0130stasyonunu (ISS) yap\u0131land\u0131rmak istiyor musunuz?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Haritada g\u00f6ster" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/uk.json b/homeassistant/components/iss/translations/uk.json new file mode 100644 index 0000000000000..cfcf3e0c458bb --- /dev/null +++ b/homeassistant/components/iss/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "\u0428\u0438\u0440\u043e\u0442\u0430 \u0442\u0430 \u0434\u043e\u0432\u0433\u043e\u0442\u0430 \u043d\u0435 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u0432 Home Assistant.", + "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": { + "user": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456?" + }, + "description": "\u0427\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u041c\u0456\u0436\u043d\u0430\u0440\u043e\u0434\u043d\u0443 \u041a\u043e\u0441\u043c\u0456\u0447\u043d\u0443 \u0421\u0442\u0430\u043d\u0446\u0456\u044e?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/zh-Hans.json b/homeassistant/components/iss/translations/zh-Hans.json new file mode 100644 index 0000000000000..47c25d7ddff3c --- /dev/null +++ b/homeassistant/components/iss/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "show_on_map": "\u5728\u5730\u56fe\u4e0a\u663e\u793a\uff1f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/zh-Hant.json b/homeassistant/components/iss/translations/zh-Hant.json new file mode 100644 index 0000000000000..f50730ff9a47d --- /dev/null +++ b/homeassistant/components/iss/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "\u5c1a\u672a\u65bc Home Assistant \u8a2d\u5b9a\u7d93\u7def\u5ea6\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\uff1f" + }, + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u570b\u969b\u592a\u7a7a\u7ad9\uff08ISS\uff09\uff1f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u65bc\u5730\u5716\u986f\u793a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 9250b567d1b33..ba152c5d84018 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -16,7 +16,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr @@ -98,10 +98,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_find_matching_config_entry(hass): +def _async_find_matching_config_entry( + hass: HomeAssistant, +) -> config_entries.ConfigEntry | None: for entry in hass.config_entries.async_entries(DOMAIN): if entry.source == config_entries.SOURCE_IMPORT: return entry + return None async def async_setup_entry( @@ -147,7 +150,7 @@ async def async_setup_entry( https = False port = host.port or 80 session = aiohttp_client.async_create_clientsession( - hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) elif host.scheme == "https": https = True @@ -206,7 +209,7 @@ async def async_setup_entry( hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback - def _async_stop_auto_update(event) -> None: + def _async_stop_auto_update(event: Event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() @@ -227,7 +230,7 @@ def _async_stop_auto_update(event) -> None: async def _async_update_listener( hass: HomeAssistant, entry: config_entries.ConfigEntry -): +) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) @@ -235,7 +238,7 @@ async def _async_update_listener( @callback def _async_import_options_from_data_if_missing( hass: HomeAssistant, entry: config_entries.ConfigEntry -): +) -> None: options = dict(entry.options) modified = False for importable_option in ( @@ -261,7 +264,7 @@ def _async_isy_to_configuration_url(isy: ISY) -> str: @callback def _async_get_or_create_isy_device_in_registry( - hass: HomeAssistant, entry: config_entries.ConfigEntry, isy + hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY ) -> None: device_registry = dr.async_get(hass) url = _async_isy_to_configuration_url(isy) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 5cf7d6b2b7659..23e77ba849d51 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,7 +1,8 @@ """Support for ISY994 binary sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta +from typing import Any from pyisy.constants import ( CMD_OFF, @@ -10,6 +11,7 @@ PROTO_INSTEON, PROTO_ZWAVE, ) +from pyisy.helpers import NodeProperty from pyisy.nodes import Group, Node from homeassistant.components.binary_sensor import ( @@ -18,7 +20,7 @@ BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -55,12 +57,25 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY994 binary sensor platform.""" - devices = [] - devices_by_address = {} - child_nodes = [] + entities: list[ + ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity + ] = [] + entities_by_address: dict[ + str, + ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity, + ] = {} + child_nodes: list[tuple[Node, str | None, str | None]] = [] + entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]: + assert isinstance(node, Node) device_class, device_type = _detect_device_type_and_class(node) if node.protocol == PROTO_INSTEON: if node.parent_node is not None: @@ -68,38 +83,38 @@ async def async_setup_entry( # nodes have been processed child_nodes.append((node, device_class, device_type)) continue - device = ISYInsteonBinarySensorEntity(node, device_class) + entity = ISYInsteonBinarySensorEntity(node, device_class) else: - device = ISYBinarySensorEntity(node, device_class) - devices.append(device) - devices_by_address[node.address] = device + entity = ISYBinarySensorEntity(node, device_class) + entities.append(entity) + entities_by_address[node.address] = entity # Handle some special child node cases for Insteon Devices for (node, device_class, device_type) in child_nodes: subnode_id = int(node.address.split(" ")[-1], 16) # Handle Insteon Thermostats - if device_type.startswith(TYPE_CATEGORY_CLIMATE): + if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE): if subnode_id == SUBNODE_CLIMATE_COOL: # Subnode 2 is the "Cool Control" sensor # It never reports its state until first use is # detected after an ISY Restart, so we assume it's off. # As soon as the ISY Event Stream connects if it has a # valid state, it will be set. - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.COLD, False ) - devices.append(device) + entities.append(entity) elif subnode_id == SUBNODE_CLIMATE_HEAT: # Subnode 3 is the "Heat Control" sensor - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.HEAT, False ) - devices.append(device) + entities.append(entity) continue if device_class in DEVICE_PARENT_REQUIRED: - parent_device = devices_by_address.get(node.parent_node.address) - if not parent_device: + parent_entity = entities_by_address.get(node.parent_node.address) + if not parent_entity: _LOGGER.error( "Node %s has a parent node %s, but no device " "was created for the parent. Skipping", @@ -115,13 +130,15 @@ async def async_setup_entry( # These sensors use an optional "negative" subnode 2 to # snag all state changes if subnode_id == SUBNODE_NEGATIVE: - parent_device.add_negative_node(node) + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) + parent_entity.add_negative_node(node) elif subnode_id == SUBNODE_HEARTBEAT: + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) # Subnode 4 is the heartbeat node, which we will # represent as a separate binary_sensor - device = ISYBinarySensorHeartbeat(node, parent_device) - parent_device.add_heartbeat_device(device) - devices.append(device) + entity = ISYBinarySensorHeartbeat(node, parent_entity) + parent_entity.add_heartbeat_device(entity) + entities.append(entity) continue if ( device_class == BinarySensorDeviceClass.MOTION @@ -133,48 +150,49 @@ async def async_setup_entry( # the initial state is forced "OFF"/"NORMAL" if the # parent device has a valid state. This is corrected # upon connection to the ISY event stream if subnode has a valid state. - initial_state = None if parent_device.state is None else False + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) + initial_state = None if parent_entity.state is None else False if subnode_id == SUBNODE_DUSK_DAWN: # Subnode 2 is the Dusk/Dawn sensor - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.LIGHT ) - devices.append(device) + entities.append(entity) continue if subnode_id == SUBNODE_LOW_BATTERY: # Subnode 3 is the low battery node - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.BATTERY, initial_state ) - devices.append(device) + entities.append(entity) continue if subnode_id in SUBNODE_TAMPER: # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # reported as "10", which translate from Hex to 10 and 16 resp. - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.PROBLEM, initial_state ) - devices.append(device) + entities.append(entity) continue if subnode_id in SUBNODE_MOTION_DISABLED: # Motion Disabled Sub-node for MS II ("D" or "13") - device = ISYInsteonBinarySensorEntity(node) - devices.append(device) + entity = ISYInsteonBinarySensorEntity(node) + entities.append(entity) continue # We don't yet have any special logic for other sensor # types, so add the nodes as individual devices - device = ISYBinarySensorEntity(node, device_class) - devices.append(device) + entity = ISYBinarySensorEntity(node, device_class) + entities.append(entity) for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: - devices.append(ISYBinarySensorProgramEntity(name, status)) + entities.append(ISYBinarySensorProgramEntity(name, status)) - await migrate_old_unique_ids(hass, BINARY_SENSOR, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, BINARY_SENSOR, entities) + async_add_entities(entities) -def _detect_device_type_and_class(node: Group | Node) -> (str, str): +def _detect_device_type_and_class(node: Group | Node) -> tuple[str | None, str | None]: try: device_type = node.type except AttributeError: @@ -199,20 +217,25 @@ def _detect_device_type_and_class(node: Group | Node) -> (str, str): class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): """Representation of a basic ISY994 binary sensor device.""" - def __init__(self, node, force_device_class=None, unknown_state=None) -> None: + def __init__( + self, + node: Node, + force_device_class: str | None = None, + unknown_state: bool | None = None, + ) -> None: """Initialize the ISY994 binary sensor device.""" super().__init__(node) self._device_class = force_device_class @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 binary sensor device is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device. This was discovered by parsing the device type code during init @@ -229,11 +252,16 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): Assistant entity and handles both ways that ISY binary sensors can work. """ - def __init__(self, node, force_device_class=None, unknown_state=None) -> None: + def __init__( + self, + node: Node, + force_device_class: str | None = None, + unknown_state: bool | None = None, + ) -> None: """Initialize the ISY994 binary sensor device.""" super().__init__(node, force_device_class) - self._negative_node = None - self._heartbeat_device = None + self._negative_node: Node | None = None + self._heartbeat_device: ISYBinarySensorHeartbeat | None = None if self._node.status == ISY_VALUE_UNKNOWN: self._computed_state = unknown_state self._status_was_unknown = True @@ -252,21 +280,21 @@ async def async_added_to_hass(self) -> None: self._async_negative_node_control_handler ) - def add_heartbeat_device(self, device) -> None: + def add_heartbeat_device(self, entity: ISYBinarySensorHeartbeat | None) -> None: """Register a heartbeat device for this sensor. The heartbeat node beats on its own, but we can gain a little reliability by considering any node activity for this sensor to be a heartbeat as well. """ - self._heartbeat_device = device + self._heartbeat_device = entity def _async_heartbeat(self) -> None: """Send a heartbeat to our heartbeat device, if we have one.""" if self._heartbeat_device is not None: self._heartbeat_device.async_heartbeat() - def add_negative_node(self, child) -> None: + def add_negative_node(self, child: Node) -> None: """Add a negative node to this binary sensor device. The negative node is a node that can receive the 'off' events @@ -287,7 +315,7 @@ def add_negative_node(self, child) -> None: self._computed_state = None @callback - def _async_negative_node_control_handler(self, event: object) -> None: + def _async_negative_node_control_handler(self, event: NodeProperty) -> None: """Handle an "On" control event from the "negative" node.""" if event.control == CMD_ON: _LOGGER.debug( @@ -299,7 +327,7 @@ def _async_negative_node_control_handler(self, event: object) -> None: self._async_heartbeat() @callback - def _async_positive_node_control_handler(self, event: object) -> None: + def _async_positive_node_control_handler(self, event: NodeProperty) -> None: """Handle On and Off control event coming from the primary node. Depending on device configuration, sometimes only On events @@ -324,7 +352,7 @@ def _async_positive_node_control_handler(self, event: object) -> None: self._async_heartbeat() @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Primary node status updates. We MOSTLY ignore these updates, as we listen directly to the Control @@ -341,7 +369,7 @@ def async_on_update(self, event: object) -> None: self._async_heartbeat() @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 binary sensor device is on. Insteon leak sensors set their primary node to On when the state is @@ -361,7 +389,14 @@ def is_on(self) -> bool: class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """Representation of the battery state of an ISY994 sensor.""" - def __init__(self, node, parent_device) -> None: + def __init__( + self, + node: Node, + parent_device: ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity, + ) -> None: """Initialize the ISY994 binary sensor device. Computed state is set to UNKNOWN unless the ISY provided a valid @@ -372,8 +407,8 @@ def __init__(self, node, parent_device) -> None: """ super().__init__(node) self._parent_device = parent_device - self._heartbeat_timer = None - self._computed_state = None + self._heartbeat_timer: CALLBACK_TYPE | None = None + self._computed_state: bool | None = None if self.state is None: self._computed_state = False @@ -386,7 +421,7 @@ async def async_added_to_hass(self) -> None: # Start the timer on bootup, so we can change from UNKNOWN to OFF self._restart_timer() - def _heartbeat_node_control_handler(self, event: object) -> None: + def _heartbeat_node_control_handler(self, event: NodeProperty) -> None: """Update the heartbeat timestamp when any ON/OFF event is sent. The ISY uses both DON and DOF commands (alternating) for a heartbeat. @@ -395,7 +430,7 @@ def _heartbeat_node_control_handler(self, event: object) -> None: self.async_heartbeat() @callback - def async_heartbeat(self): + def async_heartbeat(self) -> None: """Mark the device as online, and restart the 25 hour timer. This gets called when the heartbeat node beats, but also when the @@ -407,17 +442,14 @@ def async_heartbeat(self): self._restart_timer() self.async_write_ha_state() - def _restart_timer(self): + def _restart_timer(self) -> None: """Restart the 25 hour timer.""" - try: + if self._heartbeat_timer is not None: self._heartbeat_timer() self._heartbeat_timer = None - except TypeError: - # No heartbeat timer is active - pass @callback - def timer_elapsed(now) -> None: + def timer_elapsed(now: datetime) -> None: """Heartbeat missed; set state to ON to indicate dead battery.""" self._computed_state = True self._heartbeat_timer = None @@ -457,7 +489,7 @@ def device_class(self) -> str: return BinarySensorDeviceClass.BATTERY @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Get the state attributes for the device.""" attr = super().extra_state_attributes attr["parent_entity_id"] = self._parent_device.entity_id diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 98c20bb441ae5..00a02e5c210c7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,6 +1,8 @@ """Support for Insteon Thermostats via ISY994 Platform.""" from __future__ import annotations +from typing import Any + from pyisy.constants import ( CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_MODE, @@ -11,6 +13,7 @@ PROP_UOM, PROTO_INSTEON, ) +from pyisy.nodes import Node from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -18,9 +21,11 @@ ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE, FAN_AUTO, + FAN_OFF, FAN_ON, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -76,16 +81,15 @@ async def async_setup_entry( class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Representation of an ISY994 thermostat entity.""" - def __init__(self, node) -> None: + def __init__(self, node: Node) -> None: """Initialize the ISY Thermostat entity.""" super().__init__(node) - self._node = node self._uom = self._node.uom if isinstance(self._uom, list): self._uom = self._node.uom[0] - self._hvac_action = None - self._hvac_mode = None - self._fan_mode = None + self._hvac_action: str | None = None + self._hvac_mode: str | None = None + self._fan_mode: str | None = None self._temp_unit = None self._current_humidity = 0 self._target_temp_low = 0 @@ -97,7 +101,7 @@ def supported_features(self) -> int: return ISY_SUPPORTED_FEATURES @property - def precision(self) -> str: + def precision(self) -> float: """Return the precision of the system.""" return PRECISION_TENTHS @@ -110,6 +114,7 @@ def temperature_unit(self) -> str: return TEMP_CELSIUS if uom.value == UOM_ISY_FAHRENHEIT: return TEMP_FAHRENHEIT + return TEMP_FAHRENHEIT @property def current_humidity(self) -> int | None: @@ -119,10 +124,10 @@ def current_humidity(self) -> int | None: return int(humidity.value) @property - def hvac_mode(self) -> str | None: + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" if not (hvac_mode := self._node.aux_properties.get(CMD_CLIMATE_MODE)): - return None + return HVAC_MODE_OFF # Which state values used depends on the mode property's UOM: uom = hvac_mode.uom @@ -133,7 +138,7 @@ def hvac_mode(self) -> str | None: if self._node.protocol == PROTO_INSTEON else UOM_HVAC_MODE_GENERIC ) - return UOM_TO_STATES[uom].get(hvac_mode.value) + return UOM_TO_STATES[uom].get(hvac_mode.value, HVAC_MODE_OFF) @property def hvac_modes(self) -> list[str]: @@ -186,7 +191,7 @@ def target_temperature_low(self) -> float | None: return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1) @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return [FAN_AUTO, FAN_ON] @@ -195,10 +200,10 @@ def fan_mode(self) -> str: """Return the current fan mode ie. auto, on.""" fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING) if not fan_mode: - return None - return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value) + return FAN_OFF + return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value, FAN_OFF) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 4e700df24cbdf..866ec80040251 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,5 +1,8 @@ """Config flow for Universal Devices ISY994 integration.""" +from __future__ import annotations + import logging +from typing import Any from urllib.parse import urlparse, urlunparse from aiohttp import CookieJar @@ -38,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) -def _data_schema(schema_input): +def _data_schema(schema_input: dict[str, str]) -> vol.Schema: """Generate schema with defaults.""" return vol.Schema( { @@ -51,7 +54,9 @@ def _data_schema(schema_input): ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -65,7 +70,7 @@ async def validate_input(hass: core.HomeAssistant, data): https = False port = host.port or HTTP_PORT session = aiohttp_client.async_create_clientsession( - hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) elif host.scheme == SCHEME_HTTPS: https = True @@ -113,18 +118,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the isy994 config flow.""" - self.discovered_conf = {} + self.discovered_conf: dict[str, str] = {} @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Handle the initial step.""" errors = {} - info = None + info: dict[str, str] = {} if user_input is not None: try: info = await validate_input(self.hass, user_input) @@ -149,11 +158,15 @@ async def async_step_user(self, user_input=None): errors=errors, ) - async def async_step_import(self, user_input): + async def async_step_import( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResult: """Handle import.""" return await self.async_step_user(user_input) - async def _async_set_unique_id_or_update(self, isy_mac, ip_address, port) -> None: + async def _async_set_unique_id_or_update( + self, isy_mac: str, ip_address: str, port: int | None + ) -> None: """Abort and update the ip address on change.""" existing_entry = await self.async_set_unique_id(isy_mac) if not existing_entry: @@ -211,6 +224,7 @@ async def async_step_ssdp( """Handle a discovered isy994.""" friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info.ssdp_location + assert isinstance(url, str) parsed_url = urlparse(url) mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] if mac.startswith(UDN_UUID_PREFIX): @@ -224,6 +238,7 @@ async def async_step_ssdp( elif parsed_url.scheme == SCHEME_HTTPS: port = HTTPS_PORT + assert isinstance(parsed_url.hostname, str) await self._async_set_unique_id_or_update(mac, parsed_url.hostname, port) self.discovered_conf = { @@ -242,7 +257,9 @@ 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): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 470aaa64c6437..8ca1ac786f86d 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -204,7 +204,7 @@ # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml # Z-Wave Categories: https://www.universal-devices.com/developers/wsdk/5.0.4/4_fam.xml -NODE_FILTERS = { +NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { Platform.BINARY_SENSOR: { FILTER_UOM: [UOM_ON_OFF], FILTER_STATES: [], diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 0fcfb30a775de..f00128b6d156b 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,7 @@ """Support for ISY994 covers.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN @@ -31,53 +34,53 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 cover platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][COVER]: - devices.append(ISYCoverEntity(node)) + entities.append(ISYCoverEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]: - devices.append(ISYCoverProgramEntity(name, status, actions)) + entities.append(ISYCoverProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, COVER, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, COVER, entities) + async_add_entities(entities) class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Representation of an ISY994 cover device.""" @property - def current_cover_position(self) -> int: + def current_cover_position(self) -> int | None: """Return the current cover position.""" 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 sorted((0, self._node.status, 100))[1] + return int(sorted((0, self._node.status, 100))[1]) @property - def is_closed(self) -> bool: + def is_closed(self) -> bool | None: """Get whether the ISY994 cover device is closed.""" if self._node.status == ISY_VALUE_UNKNOWN: return None - return self._node.status == 0 + return bool(self._node.status == 0) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Send the open cover command to the ISY994 cover device.""" val = 100 if self._node.uom == UOM_BARRIER else None if not await self._node.turn_on(val=val): _LOGGER.error("Unable to open the cover") - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Send the close cover command to the ISY994 cover device.""" if not await self._node.turn_off(): _LOGGER.error("Unable to close the cover") - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] if self._node.uom == UOM_8_BIT_RANGE: @@ -94,12 +97,12 @@ def is_closed(self) -> bool: """Get whether the ISY994 cover program is closed.""" return bool(self._node.status) - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Send the open cover command to the ISY994 cover program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to open the cover") - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Send the close cover command to the ISY994 cover program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to close the cover") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index aca0dbf5d3d29..e5db8de58723f 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,6 +1,8 @@ """Representation of ISYEntity Types.""" from __future__ import annotations +from typing import Any, cast + from pyisy.constants import ( COMMAND_FRIENDLY_NAME, EMPTY_TIME, @@ -8,7 +10,9 @@ PROTO_GROUP, PROTO_ZWAVE, ) -from pyisy.helpers import NodeProperty +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node +from pyisy.programs import Program from homeassistant.const import ( ATTR_IDENTIFIERS, @@ -30,14 +34,14 @@ class ISYEntity(Entity): """Representation of an ISY994 device.""" - _name: str = None + _name: str | None = None - def __init__(self, node) -> None: + def __init__(self, node: Node) -> None: """Initialize the insteon device.""" self._node = node - self._attrs = {} - self._change_handler = None - self._control_handler = None + self._attrs: dict[str, Any] = {} + self._change_handler: EventListener | None = None + self._control_handler: EventListener | None = None async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" @@ -49,7 +53,7 @@ async def async_added_to_hass(self) -> None: ) @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Handle the update event from the ISY994 Node.""" self.async_write_ha_state() @@ -72,7 +76,7 @@ def async_on_control(self, event: NodeProperty) -> None: self.hass.bus.fire("isy994_control", event_data) @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: # not a device @@ -90,7 +94,6 @@ def device_info(self) -> DeviceInfo: basename = node.name device_info = DeviceInfo( - identifiers={}, manufacturer="Unknown", model="Unknown", name=basename, @@ -99,25 +102,30 @@ def device_info(self) -> DeviceInfo: ) if hasattr(node, "address"): - device_info[ATTR_NAME] += f" ({node.address})" + assert isinstance(node.address, str) + device_info[ATTR_NAME] = f"{basename} ({node.address})" if hasattr(node, "primary_node"): device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} # ISYv5 Device Types if hasattr(node, "node_def_id") and node.node_def_id is not None: - device_info[ATTR_MODEL] = node.node_def_id + model: str = str(node.node_def_id) # Numerical Device Type if hasattr(node, "type") and node.type is not None: - device_info[ATTR_MODEL] += f" {node.type}" + model += f" {node.type}" + device_info[ATTR_MODEL] = model if hasattr(node, "protocol"): - device_info[ATTR_MANUFACTURER] = node.protocol + model = str(device_info[ATTR_MODEL]) + manufacturer = str(node.protocol) if node.protocol == PROTO_ZWAVE: # Get extra information for Z-Wave Devices - device_info[ATTR_MANUFACTURER] += f" MfrID:{node.zwave_props.mfr_id}" - device_info[ATTR_MODEL] += ( + manufacturer += f" MfrID:{node.zwave_props.mfr_id}" + model += ( f" Type:{node.zwave_props.devtype_gen} " f"ProductTypeID:{node.zwave_props.prod_type_id} " f"ProductID:{node.zwave_props.product_id}" ) + device_info[ATTR_MANUFACTURER] = manufacturer + device_info[ATTR_MODEL] = model if hasattr(node, "folder") and node.folder is not None: device_info[ATTR_SUGGESTED_AREA] = node.folder # Note: sw_version is not exposed by the ISY for the individual devices. @@ -125,17 +133,17 @@ def device_info(self) -> DeviceInfo: return device_info @property - def unique_id(self) -> str: + def unique_id(self) -> str | None: """Get the unique identifier of the device.""" if hasattr(self._node, "address"): return f"{self._node.isy.configuration['uuid']}_{self._node.address}" return None @property - def old_unique_id(self) -> str: + def old_unique_id(self) -> str | None: """Get the old unique identifier of the device.""" if hasattr(self._node, "address"): - return self._node.address + return cast(str, self._node.address) return None @property @@ -174,7 +182,7 @@ def extra_state_attributes(self) -> dict: self._attrs.update(attr) return self._attrs - async def async_send_node_command(self, command): + async def async_send_node_command(self, command: str) -> None: """Respond to an entity service command call.""" if not hasattr(self._node, command): raise HomeAssistantError( @@ -183,8 +191,12 @@ async def async_send_node_command(self, command): await getattr(self._node, command)() async def async_send_raw_node_command( - self, command, value=None, unit_of_measurement=None, parameters=None - ): + self, + command: str, + value: Any | None = None, + unit_of_measurement: str | None = None, + parameters: Any | None = None, + ) -> None: """Respond to an entity service raw command call.""" if not hasattr(self._node, "send_cmd"): raise HomeAssistantError( @@ -192,7 +204,7 @@ async def async_send_raw_node_command( ) await self._node.send_cmd(command, value, unit_of_measurement, parameters) - async def async_get_zwave_parameter(self, parameter): + async def async_get_zwave_parameter(self, parameter: Any) -> None: """Respond to an entity service command to request a Z-Wave device parameter from the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( @@ -200,7 +212,9 @@ async def async_get_zwave_parameter(self, parameter): ) await self._node.get_zwave_parameter(parameter) - async def async_set_zwave_parameter(self, parameter, value, size): + async def async_set_zwave_parameter( + self, parameter: Any, value: Any | None, size: int | None + ) -> None: """Respond to an entity service command to set a Z-Wave device parameter via the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( @@ -209,7 +223,7 @@ async def async_set_zwave_parameter(self, parameter, value, size): await self._node.set_zwave_parameter(parameter, value, size) await self._node.get_zwave_parameter(parameter) - async def async_rename_node(self, name): + async def async_rename_node(self, name: str) -> None: """Respond to an entity service command to rename a node on the ISY.""" await self._node.rename(name) @@ -217,7 +231,7 @@ async def async_rename_node(self, name): class ISYProgramEntity(ISYEntity): """Representation of an ISY994 program base.""" - def __init__(self, name: str, status, actions=None) -> None: + def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: """Initialize the ISY994 program-based entity.""" super().__init__(status) self._name = name diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 28d0675a1849a..bf4d48ad3e801 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON @@ -27,16 +28,16 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 fan platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYFanEntity | ISYFanProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][FAN]: - devices.append(ISYFanEntity(node)) + entities.append(ISYFanEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]: - devices.append(ISYFanProgramEntity(name, status, actions)) + entities.append(ISYFanProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, FAN, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, FAN, entities) + async_add_entities(entities) class ISYFanEntity(ISYNodeEntity, FanEntity): @@ -57,11 +58,11 @@ def speed_count(self) -> int: return int_states_in_range(SPEED_RANGE) @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get if the fan is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None - return self._node.status != 0 + return bool(self._node.status != 0) async def async_set_percentage(self, percentage: int) -> None: """Set node to speed percentage for the ISY994 fan device.""" @@ -75,15 +76,15 @@ async def async_set_percentage(self, percentage: int) -> None: async def async_turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Send the turn on command to the ISY994 fan device.""" await self.async_set_percentage(percentage or 67) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 fan device.""" await self._node.turn_off() @@ -111,19 +112,19 @@ def speed_count(self) -> int: @property def is_on(self) -> bool: """Get if the fan is on.""" - return self._node.status != 0 + return bool(self._node.status != 0) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn on command to ISY994 fan program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") async def async_turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Send the turn off command to ISY994 fan program.""" if not await self._actions.run_else(): diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index d1790fcc13cf4..6d0a1d303bb15 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,7 +1,8 @@ """Sorting helpers for ISY994 device classifications.""" from __future__ import annotations -from typing import Any +from collections.abc import Sequence +from typing import TYPE_CHECKING, cast from pyisy.constants import ( ISY_VALUE_UNKNOWN, @@ -21,6 +22,7 @@ from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get_registry @@ -53,12 +55,15 @@ UOM_ISYV4_DEGREES, ) +if TYPE_CHECKING: + from .entity import ISYEntity + BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_ISY_STATES = ["on", "off"] def _check_for_node_def( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the node_def_id for any platforms. @@ -81,7 +86,7 @@ def _check_for_node_def( def _check_for_insteon_type( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the Insteon type for any platforms. @@ -146,7 +151,7 @@ def _check_for_insteon_type( def _check_for_zwave_cat( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the ISY Z-Wave Category for any platforms. @@ -176,8 +181,8 @@ def _check_for_zwave_cat( def _check_for_uom_id( hass_isy_data: dict, node: Group | Node, - single_platform: str = None, - uom_list: list = None, + single_platform: Platform | None = None, + uom_list: list[str] | None = None, ) -> bool: """Check if a node's uom matches any of the platforms uom filter. @@ -211,8 +216,8 @@ def _check_for_uom_id( def _check_for_states_in_uom( hass_isy_data: dict, node: Group | Node, - single_platform: str = None, - states_list: list = None, + single_platform: Platform | None = None, + states_list: list[str] | None = None, ) -> bool: """Check if a list of uoms matches two possible filters. @@ -247,9 +252,11 @@ def _check_for_states_in_uom( def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR): + if _check_for_node_def(hass_isy_data, node, single_platform=Platform.BINARY_SENSOR): return True - if _check_for_insteon_type(hass_isy_data, node, single_platform=BINARY_SENSOR): + if _check_for_insteon_type( + hass_isy_data, node, single_platform=Platform.BINARY_SENSOR + ): return True # For the next two checks, we're providing our own set of uoms that @@ -257,13 +264,16 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: # checks in the context of already knowing that this is definitely a # sensor device. if _check_for_uom_id( - hass_isy_data, node, single_platform=BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS + hass_isy_data, + node, + single_platform=Platform.BINARY_SENSOR, + uom_list=BINARY_SENSOR_UOMS, ): return True if _check_for_states_in_uom( hass_isy_data, node, - single_platform=BINARY_SENSOR, + single_platform=Platform.BINARY_SENSOR, states_list=BINARY_SENSOR_ISY_STATES, ): return True @@ -275,7 +285,7 @@ def _categorize_nodes( hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str ) -> None: """Sort the nodes to their proper platforms.""" - for (path, node) in nodes: + for path, node in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: # Don't import this node as a device at all @@ -365,43 +375,45 @@ def _categorize_variables( async def migrate_old_unique_ids( - hass: HomeAssistant, platform: str, devices: list[Any] | None + hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] ) -> None: """Migrate to new controller-specific unique ids.""" registry = await async_get_registry(hass) - for device in devices: + for entity in entities: + if entity.old_unique_id is None or entity.unique_id is None: + continue old_entity_id = registry.async_get_entity_id( - platform, DOMAIN, device.old_unique_id + platform, DOMAIN, entity.old_unique_id ) if old_entity_id is not None: _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", - device.old_unique_id, - device.unique_id, + entity.old_unique_id, + entity.unique_id, ) - registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) + registry.async_update_entity(old_entity_id, new_unique_id=entity.unique_id) old_entity_id_2 = registry.async_get_entity_id( - platform, DOMAIN, device.unique_id.replace(":", "") + platform, DOMAIN, entity.unique_id.replace(":", "") ) if old_entity_id_2 is not None: _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", - device.unique_id.replace(":", ""), - device.unique_id, + entity.unique_id.replace(":", ""), + entity.unique_id, ) registry.async_update_entity( - old_entity_id_2, new_unique_id=device.unique_id + old_entity_id_2, new_unique_id=entity.unique_id ) def convert_isy_value_to_hass( value: int | float | None, - uom: str, + uom: str | None, precision: int | str, fallback_precision: int | None = None, -) -> float | int: +) -> float | int | None: """Fix ISY Reported Values. ISY provides float values as an integer and precision component. @@ -416,7 +428,7 @@ def convert_isy_value_to_hass( if uom in (UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES): return round(float(value) / 2.0, 1) if precision not in ("0", 0): - return round(float(value) / 10 ** int(precision), int(precision)) + return cast(float, round(float(value) / 10 ** int(precision), int(precision))) if fallback_precision: return round(float(value), fallback_precision) return value diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 2fd98b6f17781..640442c3f19d5 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,7 +1,11 @@ """Support for ISY994 lights.""" from __future__ import annotations +from typing import Any + from pyisy.constants import ISY_VALUE_UNKNOWN +from pyisy.helpers import NodeProperty +from pyisy.nodes import Node from homeassistant.components.light import ( DOMAIN as LIGHT, @@ -35,22 +39,22 @@ async def async_setup_entry( isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) - devices = [] + entities = [] for node in hass_isy_data[ISY994_NODES][LIGHT]: - devices.append(ISYLightEntity(node, restore_light_state)) + entities.append(ISYLightEntity(node, restore_light_state)) - await migrate_old_unique_ids(hass, LIGHT, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, LIGHT, entities) + async_add_entities(entities) async_setup_light_services(hass) class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): """Representation of an ISY994 light device.""" - def __init__(self, node, restore_light_state) -> None: + def __init__(self, node: Node, restore_light_state: bool) -> None: """Initialize the ISY994 light device.""" super().__init__(node) - self._last_brightness = None + self._last_brightness: int | None = None self._restore_light_state = restore_light_state @property @@ -61,7 +65,7 @@ def is_on(self) -> bool: return int(self._node.status) != 0 @property - def brightness(self) -> float: + def brightness(self) -> int | None: """Get the brightness of the ISY994 light.""" if self._node.status == ISY_VALUE_UNKNOWN: return None @@ -70,14 +74,14 @@ def brightness(self) -> float: return round(self._node.status * 255.0 / 100.0) return int(self._node.status) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 light device.""" self._last_brightness = self.brightness if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off light") @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Save brightness in the update event from the ISY994 Node.""" if self._node.status not in (0, ISY_VALUE_UNKNOWN): self._last_brightness = self._node.status @@ -88,7 +92,7 @@ def async_on_update(self, event: object) -> None: super().async_on_update(event) # pylint: disable=arguments-differ - async def async_turn_on(self, brightness=None, **kwargs) -> None: + async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: """Send the turn on command to the ISY994 light device.""" if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness @@ -99,14 +103,14 @@ async def async_turn_on(self, brightness=None, **kwargs) -> None: _LOGGER.debug("Unable to turn on light") @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the light attributes.""" attribs = super().extra_state_attributes attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness return attribs @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS @@ -124,10 +128,10 @@ async def async_added_to_hass(self) -> None: ): self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] - async def async_set_on_level(self, value): + async def async_set_on_level(self, value: int) -> None: """Set the ON Level for a device.""" await self._node.set_on_level(value) - async def async_set_ramp_rate(self, value): + async def async_set_ramp_rate(self, value: int) -> None: """Set the Ramp Rate for a device.""" await self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index e2befc574872e..4de5cdaa05bb4 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,4 +1,7 @@ """Support for ISY994 locks.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN @@ -19,33 +22,33 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 lock platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYLockEntity | ISYLockProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][LOCK]: - devices.append(ISYLockEntity(node)) + entities.append(ISYLockEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]: - devices.append(ISYLockProgramEntity(name, status, actions)) + entities.append(ISYLockProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, LOCK, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, LOCK, entities) + async_add_entities(entities) class ISYLockEntity(ISYNodeEntity, LockEntity): """Representation of an ISY994 lock device.""" @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Get whether the lock is in locked state.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return VALUE_TO_STATE.get(self._node.status) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Send the lock command to the ISY994 device.""" if not await self._node.secure_lock(): _LOGGER.error("Unable to lock device") - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Send the unlock command to the ISY994 device.""" if not await self._node.secure_unlock(): _LOGGER.error("Unable to lock device") @@ -59,12 +62,12 @@ def is_locked(self) -> bool: """Return true if the device is locked.""" return bool(self._node.status) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" if not await self._actions.run_then(): _LOGGER.error("Unable to lock device") - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" if not await self._actions.run_else(): _LOGGER.error("Unable to unlock device") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 792629f801c21..fe0fa9720caf9 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -11,6 +11,10 @@ "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" } ], - "dhcp": [{ "hostname": "isy*", "macaddress": "0021B9*" }], - "iot_class": "local_push" + "dhcp": [ + {"registered_devices": true}, + {"hostname": "isy*", "macaddress": "0021B9*"} + ], + "iot_class": "local_push", + "loggers": ["pyisy"] } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 466a733477592..d9751fd707b07 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,6 +1,8 @@ """Support for ISY994 sensors.""" from __future__ import annotations +from typing import Any, cast + from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity @@ -29,24 +31,24 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 sensor platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] for node in hass_isy_data[ISY994_NODES][SENSOR]: _LOGGER.debug("Loading %s", node.name) - devices.append(ISYSensorEntity(node)) + entities.append(ISYSensorEntity(node)) for vname, vobj in hass_isy_data[ISY994_VARIABLES]: - devices.append(ISYSensorVariableEntity(vname, vobj)) + entities.append(ISYSensorVariableEntity(vname, vobj)) - await migrate_old_unique_ids(hass, SENSOR, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, SENSOR, entities) + async_add_entities(entities) class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" @property - def raw_unit_of_measurement(self) -> dict | str: + def raw_unit_of_measurement(self) -> dict | str | None: """Get the raw unit of measurement for the ISY994 sensor device.""" uom = self._node.uom @@ -59,12 +61,13 @@ def raw_unit_of_measurement(self) -> dict | str: return isy_states if uom in (UOM_ON_OFF, UOM_INDEX): + assert isinstance(uom, str) return uom return UOM_FRIENDLY_NAME.get(uom) @property - def native_value(self) -> str: + def native_value(self) -> float | int | str | None: """Get the state of the ISY994 sensor device.""" if (value := self._node.status) == ISY_VALUE_UNKNOWN: return None @@ -77,11 +80,11 @@ def native_value(self) -> str: return uom.get(value, value) if uom in (UOM_INDEX, UOM_ON_OFF): - return self._node.formatted + return cast(str, self._node.formatted) # Check if this is an index type and get formatted value if uom == UOM_INDEX and hasattr(self._node, "formatted"): - return self._node.formatted + return cast(str, self._node.formatted) # Handle ISY precision and rounding value = convert_isy_value_to_hass(value, uom, self._node.prec) @@ -90,10 +93,14 @@ def native_value(self) -> str: if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT): value = self.hass.config.units.temperature(value, uom) + if value is None: + return None + + assert isinstance(value, (int, float)) return value @property - def native_unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str | None: """Get the Home Assistant unit of measurement for the device.""" raw_units = self.raw_unit_of_measurement # Check if this is a known index pair UOM @@ -113,12 +120,12 @@ def __init__(self, vname: str, vobj: object) -> None: self._name = vname @property - def native_value(self): + def native_value(self) -> float | int | None: """Return the state of the variable.""" return convert_isy_value_to_hass(self._node.status, "", self._node.prec) @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Get the state attributes for the device.""" return { "init_value": convert_isy_value_to_hass( @@ -128,6 +135,6 @@ def extra_state_attributes(self) -> dict: } @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:counter" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index a1dff594a1f72..8323394803f62 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -1,4 +1,5 @@ """ISY Services and Commands.""" +from __future__ import annotations from typing import Any @@ -93,6 +94,7 @@ def valid_isy_commands(value: Any) -> str: """Validate the command is valid.""" value = str(value).upper() if value in COMMAND_FRIENDLY_NAME: + assert isinstance(value, str) return value raise vol.Invalid("Invalid ISY Command.") @@ -173,7 +175,7 @@ def valid_isy_commands(value: Any) -> str: @callback -def async_setup_services(hass: HomeAssistant): # noqa: C901 +def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Create and register services for the ISY integration.""" existing_services = hass.services.async_services().get(DOMAIN) if existing_services and any( @@ -234,7 +236,7 @@ async def async_send_program_command_service_handler(service: ServiceCall) -> No """Handle a send program command service call.""" address = service.data.get(CONF_ADDRESS) name = service.data.get(CONF_NAME) - command = service.data.get(CONF_COMMAND) + command = service.data[CONF_COMMAND] isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: @@ -432,7 +434,7 @@ async def _async_rename_node(call: ServiceCall) -> None: @callback -def async_unload_services(hass: HomeAssistant): +def async_unload_services(hass: HomeAssistant) -> None: """Unload services for the ISY integration.""" if hass.data[DOMAIN]: # There is still another config entry for this domain, don't remove services. @@ -456,7 +458,7 @@ def async_unload_services(hass: HomeAssistant): @callback -def async_setup_light_services(hass: HomeAssistant): +def async_setup_light_services(hass: HomeAssistant) -> None: """Create device-specific services for the ISY Integration.""" platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 3e72dd6f0ecd9..a92be5d4d23c8 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,4 +1,7 @@ """Support for ISY994 switches.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP @@ -17,39 +20,39 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 switch platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] for node in hass_isy_data[ISY994_NODES][SWITCH]: - devices.append(ISYSwitchEntity(node)) + entities.append(ISYSwitchEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]: - devices.append(ISYSwitchProgramEntity(name, status, actions)) + entities.append(ISYSwitchProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, SWITCH, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, SWITCH, entities) + async_add_entities(entities) class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): """Representation of an ISY994 switch device.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 device is in the on state.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 switch.""" if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off switch") - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY994 switch.""" if not await self._node.turn_on(): _LOGGER.debug("Unable to turn on switch") @property - def icon(self) -> str: + def icon(self) -> str | None: """Get the icon for groups.""" if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: return "mdi:google-circles-communities" # Matches isy scene icon @@ -64,12 +67,12 @@ def is_on(self) -> bool: """Get whether the ISY994 switch program is on.""" return bool(self._node.status) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY994 switch program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn on switch") - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 switch program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to turn off switch") diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index f550b8ed07bfd..a8497ba0b2793 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -1,7 +1,12 @@ """Provide info to system health.""" +from __future__ import annotations + +from typing import Any + from pyisy import ISY from homeassistant.components import system_health +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback @@ -16,7 +21,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" health_info = {} @@ -26,6 +31,7 @@ async def system_health_info(hass): isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] entry = hass.config_entries.async_get_entry(config_entry_id) + assert isinstance(entry, ConfigEntry) health_info["host_reachable"] = await system_health.async_check_can_reach_url( hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}" ) diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index eaec481025c38..470975e211767 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -1,5 +1,48 @@ { "config": { - "flow_title": "{name} ({host})" + "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": "v", + "invalid_host": "\u0397 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03c3\u03b5 \u03c0\u03bb\u03ae\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL, \u03c0.\u03c7. http://192.168.10.100:80", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "tls": "\u0397 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 TLS \u03c4\u03bf\u03c5 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae ISY.", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0397 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03bb\u03ae\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL, \u03c0.\u03c7. http://192.168.10.100:80", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\u03a0\u03b1\u03c1\u03ac\u03b2\u03bb\u03b5\u03c8\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac\u03c2", + "restore_light_state": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2", + "sensor_string": "\u03a3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5", + "variable_sensor_string": "\u039c\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + }, + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 ISY: \n - \u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5: \u039a\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 'Node Sensor String' \u03c3\u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03c4\u03b9\u03bc\u03b5\u03c4\u03c9\u03c0\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03ae \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n - Ignore String (\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac): \u039f\u03c0\u03bf\u03b9\u03b1\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03c4\u03bf 'Ignore String' \u03c3\u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b5\u03af\u03c4\u03b1\u03b9. \n - \u039c\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1: \u039a\u03ac\u03b8\u03b5 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf 'Variable Sensor String' \u03b8\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af \u03c9\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n - \u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2: \u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03ba\u03b1\u03b8\u03af\u03c3\u03c4\u03b1\u03c4\u03b1\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ISY994" + } + } + }, + "system_health": { + "info": { + "device_connected": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf ISY", + "host_reachable": "\u039f\u03b9\u03ba\u03bf\u03b4\u03b5\u03c3\u03c0\u03cc\u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf\u03c2", + "last_heartbeat": "\u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf\u03c2 \u03c7\u03c4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c1\u03b4\u03b9\u03ac\u03c2", + "websocket_status": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index f1021cbd6e957..271cba9e0c489 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -39,7 +39,7 @@ }, "system_health": { "info": { - "device_connected": "ISY\u63a5\u7d9a\u6e08", + "device_connected": "ISY\u63a5\u7d9a\u6e08\u307f", "host_reachable": "\u30db\u30b9\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd", "last_heartbeat": "\u6700\u5f8c\u306e\u30cf\u30fc\u30c8\u30d3\u30fc\u30c8\u30bf\u30a4\u30e0", "websocket_status": "\u30a4\u30d9\u30f3\u30c8\u30bd\u30b1\u30c3\u30c8 \u30b9\u30c6\u30fc\u30bf\u30b9" diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index 2bc8cd2ef5aa2..b644b6c1bfc00 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -1,15 +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_host": "A entrada do host n\u00e3o est\u00e1 no formato de URL completo, por exemplo, http://192.168.10.100:80", - "unknown": "Erro inesperado." + "unknown": "Erro inesperado" }, - "flow_title": "Dispositivos universais ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "URL", - "tls": "A vers\u00e3o TLS do controlador ISY." + "password": "Senha", + "tls": "A vers\u00e3o TLS do controlador ISY.", + "username": "Usu\u00e1rio" }, "description": "A entrada do endere\u00e7o deve estar no formato de URL completo, por exemplo, http://192.168.10.100:80", "title": "Conecte-se ao seu ISY994" @@ -25,8 +32,17 @@ "sensor_string": "Texto do sensor node", "variable_sensor_string": "Texto da vari\u00e1vel do sensor" }, + "description": "Defina as op\u00e7\u00f5es para a Integra\u00e7\u00e3o ISY:\n \u2022 Cadeia de Sensores de N\u00f3: Qualquer dispositivo ou pasta que contenha 'Cadeia de Sensores de N\u00f3' no nome ser\u00e1 tratado como um sensor ou sensor bin\u00e1rio.\n \u2022 Ignore String: Qualquer dispositivo com 'Ignore String' no nome ser\u00e1 ignorado.\n \u2022 Variable Sensor String: Qualquer vari\u00e1vel que contenha 'Variable Sensor String' ser\u00e1 adicionada como sensor.\n \u2022 Restaurar o brilho da luz: Se ativado, o brilho anterior ser\u00e1 restaurado ao acender uma luz em vez do n\u00edvel integrado do dispositivo.", "title": "ISY994 Op\u00e7\u00f5es" } } + }, + "system_health": { + "info": { + "device_connected": "ISY conectado", + "host_reachable": "Alcance do host", + "last_heartbeat": "Hora da \u00faltima pulsa\u00e7\u00e3o", + "websocket_status": "Status do soquete de eventos" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/sk.json b/homeassistant/components/isy994/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/isy994/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json index c874b8654f58d..e50bc5cb26b61 100644 --- a/homeassistant/components/isy994/translations/uk.json +++ b/homeassistant/components/isy994/translations/uk.json @@ -29,10 +29,10 @@ "data": { "ignore_string": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438", "restore_light_state": "\u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430", - "sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440", + "sensor_string": "\u0420\u044f\u0434\u043e\u043a \u0441\u0435\u043d\u0441\u043e\u0440\u0443 \u0432\u0443\u0437\u043b\u0430", "variable_sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440" }, - "description": "\u041e\u043f\u0438\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432:\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u043f\u0430\u043f\u043a\u0430, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0457 \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u043e \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0430\u0431\u043e \u0431\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0430 \u0437\u043c\u0456\u043d\u043d\u0430, \u044f\u043a\u0430 \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0433\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f.\n \u2022 \u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0434\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "description": "\u041e\u043f\u0438\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432:\n \u2022 \u0420\u044f\u0434\u043e\u043a \u0441\u0435\u043d\u0441\u043e\u0440\u0443 \u0432\u0443\u0437\u043b\u0430: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u0442\u0435\u043a\u0430, \u0449\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u0438\u043c\u0435 \"\u0440\u044f\u0434\u043e\u043a \u0441\u0435\u043d\u0441\u043e\u0440\u0443 \u0432\u0443\u0437\u043b\u0430\" \u0432 \u0456\u043c\u0435\u043d\u0456, \u0431\u0443\u0434\u0435 \u0432\u0438\u0437\u043d\u0430\u043d\u043e \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0430\u0431\u043e \u0431\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0430 \u0437\u043c\u0456\u043d\u043d\u0430, \u044f\u043a\u0430 \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0433\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f.\n \u2022 \u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0434\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ISY994" } } diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index 83a77e1257904..bc4fb8ceddcc0 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -6,7 +6,7 @@ from async_timeout import timeout -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: controller_ready = asyncio.Event() diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 9cdf30ad42b9a..b86e86e2b5899 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -8,5 +8,6 @@ "homekit": { "models": ["iZone"] }, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pizone"] } diff --git a/homeassistant/components/izone/translations/el.json b/homeassistant/components/izone/translations/el.json new file mode 100644 index 0000000000000..341001a0898dd --- /dev/null +++ b/homeassistant/components/izone/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf iZone;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/pt-BR.json b/homeassistant/components/izone/translations/pt-BR.json new file mode 100644 index 0000000000000..d9055af4b36e0 --- /dev/null +++ b/homeassistant/components/izone/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja configurar o iZone?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/uk.json b/homeassistant/components/izone/translations/uk.json index 8ab6c1e166437..e29c169a26e09 100644 --- a/homeassistant/components/izone/translations/uk.json +++ b/homeassistant/components/izone/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/izone/translations/zh-Hant.json b/homeassistant/components/izone/translations/zh-Hant.json index 363e62a1b5ff9..7a35966f6853e 100644 --- a/homeassistant/components/izone/translations/zh-Hant.json +++ b/homeassistant/components/izone/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 345cecc2eb66e..ce00edfc10838 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -9,5 +9,6 @@ "iot_class": "local_polling", "codeowners": [ "@j-stienstra" - ] + ], + "loggers": ["jellyfin_apiclient_python"] } \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/cs.json b/homeassistant/components/jellyfin/translations/cs.json new file mode 100644 index 0000000000000..c9a6f8f2462e5 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/el.json b/homeassistant/components/jellyfin/translations/el.json new file mode 100644 index 0000000000000..415cc4256f33c --- /dev/null +++ b/homeassistant/components/jellyfin/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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", + "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": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/nb.json b/homeassistant/components/jellyfin/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/pt-BR.json b/homeassistant/components/jellyfin/translations/pt-BR.json new file mode 100644 index 0000000000000..2fda26fe56631 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "url": "URL", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/sk.json b/homeassistant/components/jellyfin/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/jellyfin/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/zh-Hant.json b/homeassistant/components/jellyfin/translations/zh-Hant.json index 3f24589c23521..886d6e3676eab 100644 --- a/homeassistant/components/jellyfin/translations/zh-Hant.json +++ b/homeassistant/components/jellyfin/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index ef77dc045806d..9077fef50fdaf 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": ["hdate==0.10.4"], "codeowners": ["@tsvi"], - "iot_class": "calculated" + "iot_class": "calculated", + "loggers": ["hdate"] } diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index a9d67e915fa49..b56f4a091f085 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": ["python-join-api==0.0.6"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pyjoin"] } diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index d56977dc9df47..35e9414a1e60a 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -5,5 +5,6 @@ "requirements": ["python-juicenet==1.0.2"], "codeowners": ["@jesserockz"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyjuicenet"] } diff --git a/homeassistant/components/juicenet/translations/el.json b/homeassistant/components/juicenet/translations/el.json new file mode 100644 index 0000000000000..2ca8a3c7cfa92 --- /dev/null +++ b/homeassistant/components/juicenet/translations/el.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b1\u03c0\u03cc https://home.juice.net/Manage.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/it.json b/homeassistant/components/juicenet/translations/it.json index 90e3ccd3c17f5..489f29db7a6e0 100644 --- a/homeassistant/components/juicenet/translations/it.json +++ b/homeassistant/components/juicenet/translations/it.json @@ -14,7 +14,7 @@ "api_token": "Token API" }, "description": "Avrete bisogno del Token API da https://home.juice.net/Manage.", - "title": "Connettersi a JuiceNet" + "title": "Connettiti a JuiceNet" } } } diff --git a/homeassistant/components/juicenet/translations/pt-BR.json b/homeassistant/components/juicenet/translations/pt-BR.json index 281a9dc89312e..c5cd5d0dcc91e 100644 --- a/homeassistant/components/juicenet/translations/pt-BR.json +++ b/homeassistant/components/juicenet/translations/pt-BR.json @@ -1,9 +1,21 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_token": "Token da API" + }, + "description": "Voc\u00ea precisar\u00e1 do token de API de https://home.juice.net/Manage.", + "title": "Conecte-se ao JuiceNet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/sk.json b/homeassistant/components/juicenet/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/juicenet/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index f34ae161c6dd1..53cc89e708e89 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -99,5 +99,7 @@ async def async_update(self) -> None: self.data[self._devices_ids[i]] = device except IndexError as err: _LOGGER.error("Parsing error %s", err) + except TypeError as err: + _LOGGER.error("Type error %s", err) async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 1bdcd7670e64e..9f2a4c0013f54 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/kaiterra", "requirements": ["kaiterra-async-client==0.0.2"], "codeowners": ["@Michsior14"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["kaiterra_async_client"] } diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 7e148be103b0a..e1685cd47c320 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==1.1.0"], "codeowners": ["@dannerph"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["keba_kecontact"] } diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 8a494b88d9111..ecd9ece1bd0a5 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -async def update_listener(hass, entry): +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/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 3f01c9091c7e7..be1ffd1f0b224 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -15,5 +15,6 @@ } ], "codeowners": ["@foxel"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ndms2_client"] } diff --git a/homeassistant/components/keenetic_ndms2/translations/el.json b/homeassistant/components/keenetic_ndms2/translations/el.json index 36a89b1744943..1f8a5b838f3f7 100644 --- a/homeassistant/components/keenetic_ndms2/translations/el.json +++ b/homeassistant/components/keenetic_ndms2/translations/el.json @@ -1,8 +1,33 @@ { + "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", + "no_udn": "\u039f\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 SSDP \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd UDN", + "not_keenetic_ndms2": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 Keenetic" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Keenetic NDMS2" + } + } + }, "options": { "step": { "user": { "data": { + "consider_home": "\u0395\u03be\u03b5\u03c4\u03ac\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03c4\u03bf \u03c3\u03c0\u03af\u03c4\u03b9", + "include_arp": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd ARP (\u03b1\u03b3\u03bd\u03bf\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 hotspot)", + "include_associated": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03c5\u03c3\u03c7\u03b5\u03c4\u03af\u03c3\u03b5\u03c9\u03bd WiFi AP (\u03b1\u03b3\u03bd\u03bf\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 hotspot)", "interfaces": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2", "try_hotspot": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd 'ip hotspot' (\u03c0\u03b9\u03bf \u03b1\u03ba\u03c1\u03b9\u03b2\u03ad\u03c2)" diff --git a/homeassistant/components/keenetic_ndms2/translations/nb.json b/homeassistant/components/keenetic_ndms2/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/pt-BR.json b/homeassistant/components/keenetic_ndms2/translations/pt-BR.json new file mode 100644 index 0000000000000..c143db7cbd1ae --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/pt-BR.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "no_udn": "As informa\u00e7\u00f5es de descoberta SSDP n\u00e3o t\u00eam UDN", + "not_keenetic_ndms2": "O item descoberto n\u00e3o \u00e9 um roteador Keenetic" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name} ( {host} )", + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar o roteador Keenetic NDMS2" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Considere o intervalo em casa", + "include_arp": "Use dados ARP (ignorados se forem usados dados de hotspot)", + "include_associated": "Use dados de associa\u00e7\u00f5es de AP WiFi (ignorado se forem usados dados de ponto de acesso)", + "interfaces": "Escolha interfaces para escanear", + "scan_interval": "Intervalo de escaneamento", + "try_hotspot": "Use dados 'ip hotspot' (mais precisos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/sk.json b/homeassistant/components/keenetic_ndms2/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 1b0c0b190e603..40365aa860cc2 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "codeowners": ["@basnijholt"], "requirements": ["aiokef==0.2.16", "getmac==0.8.2"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiokef", "tenacity"] } diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index b53d44ff18838..8e8d982d21628 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": ["pyuserinput==0.1.11"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pykeyboard"] } diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 1fc34f4700042..76ab1d7cf5c83 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": ["evdev==1.4.0", "aionotify==0.2.0"], "codeowners": ["@bendavid", "@lanrat"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aionotify", "evdev"] } diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index 09514d01cb51b..a65af141e1598 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": ["pykira==0.1.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pykira"] } diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index 7b5093eb86b5d..8185c3000536e 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": ["kiwiki-client==0.1.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["kiwiki"] } diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json index 1c17ee0fd3cfd..0fab41e103eca 100644 --- a/homeassistant/components/kmtronic/manifest.json +++ b/homeassistant/components/kmtronic/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/kmtronic", "requirements": ["pykmtronic==0.3.0"], "codeowners": ["@dgomes"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pykmtronic"] } diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index 26cce2c736d41..e941a2ffafac6 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -1,11 +1,13 @@ """KMtronic Switch integration.""" +import urllib.parse + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN +from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER async def async_setup_entry( @@ -19,7 +21,7 @@ async def async_setup_entry( async_add_entities( [ - KMtronicSwitch(coordinator, relay, reverse, entry.entry_id) + KMtronicSwitch(hub, coordinator, relay, reverse, entry.entry_id) for relay in hub.relays ] ) @@ -28,22 +30,22 @@ async def async_setup_entry( class KMtronicSwitch(CoordinatorEntity, SwitchEntity): """KMtronic Switch Entity.""" - def __init__(self, coordinator, relay, reverse, config_entry_id): + def __init__(self, hub, coordinator, relay, reverse, config_entry_id): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._relay = relay - self._config_entry_id = config_entry_id self._reverse = reverse - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"Relay{self._relay.id}" + hostname = urllib.parse.urlsplit(hub.host).hostname + self._attr_device_info = { + "identifiers": {(DOMAIN, config_entry_id)}, + "name": f"Controller {hostname}", + "manufacturer": MANUFACTURER, + "configuration_url": hub.host, + } - @property - def unique_id(self) -> str: - """Return the unique ID of the entity.""" - return f"{self._config_entry_id}_relay{self._relay.id}" + self._attr_name = f"Relay{relay.id}" + self._attr_unique_id = f"{config_entry_id}_relay{relay.id}" @property def is_on(self): diff --git a/homeassistant/components/kmtronic/translations/el.json b/homeassistant/components/kmtronic/translations/el.json index 53d33e27e8fa7..6f186c16e3bf4 100644 --- a/homeassistant/components/kmtronic/translations/el.json +++ b/homeassistant/components/kmtronic/translations/el.json @@ -1,4 +1,23 @@ { + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/kmtronic/translations/nb.json b/homeassistant/components/kmtronic/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/pt-BR.json b/homeassistant/components/kmtronic/translations/pt-BR.json new file mode 100644 index 0000000000000..2d0b50c0640fa --- /dev/null +++ b/homeassistant/components/kmtronic/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "L\u00f3gica de comuta\u00e7\u00e3o reversa (use NC)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/sk.json b/homeassistant/components/kmtronic/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index cdaf5c73e74ae..5c2d7e3b68c59 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -225,9 +225,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - conf = hass.data.get(DATA_KNX_CONFIG) # `conf` is None when reloading the integration or no `knx` key in configuration.yaml - if conf is None: + if (conf := hass.data.get(DATA_KNX_CONFIG)) is None: _conf = await async_integration_yaml_config(hass, DOMAIN) if not _conf or DOMAIN not in _conf: _LOGGER.warning( @@ -546,7 +545,7 @@ async def service_exposure_register_modify(self, call: ServiceCall) -> None: replaced_exposure.device.name, ) replaced_exposure.shutdown() - exposure = create_knx_exposure(self.hass, self.xknx, call.data) # type: ignore[arg-type] + exposure = create_knx_exposure(self.hass, self.xknx, call.data) self.service_exposures[group_address] = exposure _LOGGER.debug( "Service exposure_register registered exposure for '%s' - %s", diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 4f7a9d6723cdb..6bc6085d0e56a 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -317,7 +317,7 @@ async def async_step_init( CONF_KNX_TUNNELING, CONF_KNX_ROUTING, ] - self.current_config = self.config_entry.data # type: ignore + self.current_config = self.config_entry.data # type: ignore[assignment] data_schema = { vol.Required( diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 663c0e5839a77..924fa284e9308 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,5 +12,6 @@ "@marvin-w" ], "quality_scale": "silver", - "iot_class": "local_push" -} \ No newline at end of file + "iot_class": "local_push", + "loggers": ["xknx"] +} diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index e946266f8f4c7..f5f8875bb5fc8 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -130,7 +130,7 @@ def numeric_type_validator(value: Any) -> str | int: def _max_payload_value(payload_length: int) -> int: if payload_length == 0: return 0x3F - return int(256 ** payload_length) - 1 + return int(256**payload_length) - 1 def button_payload_sub_validator(entity_config: OrderedDict) -> OrderedDict: diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 0778524902697..43f72f4986787 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -12,7 +12,8 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\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)", - "port": "\u041f\u043e\u0440\u0442" + "port": "\u041f\u043e\u0440\u0442", + "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" } }, "routing": { @@ -33,7 +34,8 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\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)", - "port": "\u041f\u043e\u0440\u0442" + "port": "\u041f\u043e\u0440\u0442", + "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" } } } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index 6778b3539a02c..dd553f293be63 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -14,7 +14,8 @@ "individual_address": "Adre\u00e7a individual de la connexi\u00f3", "local_ip": "IP local de Home Assistant (deixa-ho en blanc si no n'est\u00e0s segur/a)", "port": "Port", - "route_back": "Encaminament de retorn / Mode NAT" + "route_back": "Encaminament de retorn / Mode NAT", + "tunneling_type": "Tipus de t\u00fanel KNX" }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." }, @@ -59,7 +60,8 @@ "host": "Amfitri\u00f3", "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "port": "Port", - "route_back": "Encaminament de retorn / Mode NAT" + "route_back": "Encaminament de retorn / Mode NAT", + "tunneling_type": "Tipus de t\u00fanel KNX" } } } diff --git a/homeassistant/components/knx/translations/cs.json b/homeassistant/components/knx/translations/cs.json new file mode 100644 index 0000000000000..31c65f915ddde --- /dev/null +++ b/homeassistant/components/knx/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "manual_tunnel": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index cc6af948b5b74..6716b5c8a3776 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -14,7 +14,8 @@ "individual_address": "Individuelle Adresse f\u00fcr die Verbindung", "local_ip": "Lokale IP des Home Assistant (f\u00fcr automatische Erkennung leer lassen)", "port": "Port", - "route_back": "Route Back / NAT-Modus" + "route_back": "Route Back / NAT-Modus", + "tunneling_type": "KNX Tunneling Typ" }, "description": "Bitte gib die Verbindungsinformationen deines Tunnelger\u00e4ts ein." }, @@ -59,7 +60,8 @@ "host": "Host", "local_ip": "Lokale IP (leer lassen, wenn unsicher)", "port": "Port", - "route_back": "Route Back / NAT-Modus" + "route_back": "Route Back / NAT-Modus", + "tunneling_type": "KNX Tunneling Typ" } } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 19ee8d73aa405..75be33560ca4b 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -1,17 +1,28 @@ { "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", + "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" + }, "step": { "manual_tunnel": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "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", "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)", - "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT" + "port": "\u0398\u03cd\u03c1\u03b1", + "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", + "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "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." }, "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" }, @@ -37,12 +48,20 @@ "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 \u03b1\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\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" + "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" } }, "tunnel": { "data": { - "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b1\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9)" + "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 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b1\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9)", + "port": "\u0398\u03cd\u03c1\u03b1", + "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", + "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 93ba7c006f047..b8b8cf1250e41 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -10,11 +10,12 @@ "step": { "manual_tunnel": { "data": { - "tunneling_type": "KNX Tunneling Type", "host": "Host", "individual_address": "Individual address for the connection", "local_ip": "Local IP of Home Assistant (leave empty for automatic detection)", - "port": "Port" + "port": "Port", + "route_back": "Route Back / NAT Mode", + "tunneling_type": "KNX Tunneling Type" }, "description": "Please enter the connection information of your tunneling device." }, @@ -56,10 +57,11 @@ }, "tunnel": { "data": { - "tunneling_type": "KNX Tunneling Type", "host": "Host", "local_ip": "Local IP (leave empty if unsure)", - "port": "Port" + "port": "Port", + "route_back": "Route Back / NAT Mode", + "tunneling_type": "KNX Tunneling Type" } } } diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index 712015e6c91ee..e9417cdac37ee 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -14,7 +14,8 @@ "individual_address": "\u00dchenduse individuaalne aadress", "local_ip": "Home Assistanti kohalik IP (automaatseks tuvastuseks j\u00e4ta t\u00fchjaks)", "port": "Port", - "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", + "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, "description": "Sisesta tunneldamisseadme \u00fchenduse teave." }, @@ -59,7 +60,8 @@ "host": "Host", "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks, kui ei ole kindel)", "port": "Port", - "route_back": "Marsruudi tagasitee / NAT-re\u017eiim" + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", + "tunneling_type": "KNX tunneli t\u00fc\u00fcp" } } } diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 763d028803a22..08e53ad8d4952 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -14,7 +14,8 @@ "individual_address": "Adresse individuelle pour la connexion", "local_ip": "IP locale (laisser vide en cas de doute)", "port": "Port", - "route_back": "Retour/Mode NAT" + "route_back": "Retour/Mode NAT", + "tunneling_type": "Type de tunnel KNX" }, "description": "Veuillez saisir les informations de connexion de votre p\u00e9riph\u00e9rique de tunneling." }, @@ -59,7 +60,8 @@ "host": "H\u00f4te", "local_ip": "IP locale (laisser vide en cas de doute)", "port": "Port", - "route_back": "Retour/Mode NAT" + "route_back": "Retour/Mode NAT", + "tunneling_type": "Type de tunnel KNX" } } } diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index acc102240a397..13bd465037826 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -14,7 +14,8 @@ "individual_address": "A kapcsolat egy\u00e9ni c\u00edme", "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "port": "Port", - "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" + "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", + "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." }, @@ -59,7 +60,8 @@ "host": "C\u00edm", "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "port": "Port", - "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d" + "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", + "tunneling_type": "KNX alag\u00fat t\u00edpusa" } } } diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index e93689f3bec98..1cdb614a3cc28 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -12,16 +12,17 @@ "data": { "host": "Host", "individual_address": "Alamat individu untuk koneksi", - "local_ip": "IP lokal (kosongkan jika tidak yakin)", + "local_ip": "IP lokal Home Assistant (kosongkan jika tidak yakin)", "port": "Port", - "route_back": "Dirutekan Kembali/Mode NAT" + "route_back": "Dirutekan Kembali/Mode NAT", + "tunneling_type": "Jenis Tunnel KNX" }, "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." }, "routing": { "data": { "individual_address": "Alamat individu untuk koneksi routing", - "local_ip": "IP lokal (kosongkan jika tidak yakin)", + "local_ip": "IP lokal Home Assistant (kosongkan jika tidak yakin)", "multicast_group": "Grup multicast yang digunakan untuk routing", "multicast_port": "Port multicast yang digunakan untuk routing" }, @@ -47,7 +48,7 @@ "data": { "connection_type": "Jenis Koneksi KNX", "individual_address": "Alamat individu default", - "local_ip": "IP lokal (kosongkan jika tidak yakin)", + "local_ip": "IP lokal Home Assistant (gunakan 0.0.0.0 untuk deteksi otomatis)", "multicast_group": "Grup multicast yang digunakan untuk routing dan penemuan", "multicast_port": "Port multicast yang digunakan untuk routing dan penemuan", "rate_limit": "Jumlah maksimal telegram keluar per detik", @@ -59,7 +60,8 @@ "host": "Host", "local_ip": "IP lokal (kosongkan jika tidak yakin)", "port": "Port", - "route_back": "Dirutekan Kembali/Mode NAT" + "route_back": "Dirutekan Kembali/Mode NAT", + "tunneling_type": "Jenis Tunnel KNX" } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 2c6d11e073c88..ad4e9f3461053 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -14,7 +14,8 @@ "individual_address": "Indirizzo individuale per la connessione", "local_ip": "IP locale di Home Assistant (lascia vuoto per il rilevamento automatico)", "port": "Porta", - "route_back": "Torna indietro / Modalit\u00e0 NAT" + "route_back": "Torna indietro / Modalit\u00e0 NAT", + "tunneling_type": "Tipo tunnel KNX" }, "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." }, @@ -59,7 +60,8 @@ "host": "Host", "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "port": "Porta", - "route_back": "Torna indietro / Modalit\u00e0 NAT" + "route_back": "Torna indietro / Modalit\u00e0 NAT", + "tunneling_type": "Tipo tunnel KNX" } } } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 83a11ef82fea6..a4744a41a2cbc 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -14,7 +14,8 @@ "individual_address": "\u63a5\u7d9a\u7528\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", - "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" + "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", + "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" }, "description": "\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, @@ -59,7 +60,8 @@ "host": "\u30db\u30b9\u30c8", "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", - "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9" + "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", + "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 0d0bbffce142d..9b68bd02d2f4a 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -14,7 +14,8 @@ "individual_address": "Individueel adres voor de verbinding", "local_ip": "Lokaal IP van Home Assistant (leeg laten voor automatische detectie)", "port": "Poort", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "tunneling_type": "KNX Tunneling Type" }, "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." }, @@ -59,7 +60,8 @@ "host": "Host", "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "port": "Poort", - "route_back": "Route Back / NAT Mode" + "route_back": "Route Back / NAT Mode", + "tunneling_type": "KNX Tunneling Type" } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 4f0d8008d254f..231945d52336a 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -14,7 +14,8 @@ "individual_address": "Individuell adresse for tilkoblingen", "local_ip": "Lokal IP for Home Assistant (la st\u00e5 tomt for automatisk gjenkjenning)", "port": "Port", - "route_back": "Rute tilbake / NAT-modus" + "route_back": "Rute tilbake / NAT-modus", + "tunneling_type": "KNX tunneltype" }, "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." }, @@ -59,7 +60,8 @@ "host": "Vert", "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "port": "Port", - "route_back": "Rute tilbake / NAT-modus" + "route_back": "Rute tilbake / NAT-modus", + "tunneling_type": "KNX tunneltype" } } } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index 17770f9e1ddab..8a30af3fd5904 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -14,7 +14,8 @@ "individual_address": "Indywidualny adres dla po\u0142\u0105czenia", "local_ip": "Lokalny adres IP Home Assistant (pozostaw puste w celu automatycznego wykrywania)", "port": "Port", - "route_back": "Tryb Route Back / NAT" + "route_back": "Tryb Route Back / NAT", + "tunneling_type": "Typ tunelowania KNX" }, "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." }, @@ -59,7 +60,8 @@ "host": "Nazwa hosta lub adres IP", "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "port": "Port", - "route_back": "Tryb Route Back / NAT" + "route_back": "Tryb Route Back / NAT", + "tunneling_type": "Typ tunelowania KNX" } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json new file mode 100644 index 0000000000000..0e8e340296187 --- /dev/null +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "manual_tunnel": { + "data": { + "host": "Nome do host", + "individual_address": "Endere\u00e7o individual para a conex\u00e3o", + "local_ip": "IP local do Home Assistant (deixe em branco para detec\u00e7\u00e3o autom\u00e1tica)", + "port": "Porta", + "route_back": "Modo Rota de Retorno / NAT", + "tunneling_type": "Tipo de t\u00fanel KNX" + }, + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + }, + "routing": { + "data": { + "individual_address": "Endere\u00e7o individual para a conex\u00e3o de roteamento", + "local_ip": "IP local do Home Assistant (deixe vazio para detec\u00e7\u00e3o autom\u00e1tica)", + "multicast_group": "O grupo multicast usado para roteamento", + "multicast_port": "A porta multicast usada para roteamento" + }, + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." + }, + "tunnel": { + "data": { + "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": { + "step": { + "init": { + "data": { + "connection_type": "Tipo de conex\u00e3o KNX", + "individual_address": "Endere\u00e7o individual padr\u00e3o", + "local_ip": "IP local do Home Assistant (use 0.0.0.0 para detec\u00e7\u00e3o autom\u00e1tica)", + "multicast_group": "Grupo multicast usado para roteamento e descoberta", + "multicast_port": "Porta multicast usada para roteamento e descoberta", + "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo", + "state_updater": "Permitir globalmente estados de leitura a partir do KNX Bus" + } + }, + "tunnel": { + "data": { + "host": "Nome do host", + "local_ip": "IP local (deixe em branco se n\u00e3o tiver certeza)", + "port": "Porta", + "route_back": "Modo Rota de Retorno / NAT", + "tunneling_type": "Tipo de t\u00fanel KNX" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 0955b54fda872..6c1a41dac914d 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -14,7 +14,8 @@ "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f)", "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" + "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" }, "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." }, @@ -59,7 +60,8 @@ "host": "\u0425\u043e\u0441\u0442", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "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" + "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" } } } diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json new file mode 100644 index 0000000000000..6668aaa92fb56 --- /dev/null +++ b/homeassistant/components/knx/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "manual_tunnel": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json new file mode 100644 index 0000000000000..b1be9557565ea --- /dev/null +++ b/homeassistant/components/knx/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "manual_tunnel": { + "data": { + "tunneling_type": "KNX tunneltyp" + } + } + } + }, + "options": { + "step": { + "tunnel": { + "data": { + "tunneling_type": "KNX tunneltyp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index fc476a776a417..6267038a6e023 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -14,7 +14,8 @@ "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", "local_ip": "Home Assistant Yerel IP'si (otomatik alg\u0131lama i\u00e7in bo\u015f b\u0131rak\u0131n)", "port": "Port", - "route_back": "Geri Y\u00f6nlendirme / NAT Modu" + "route_back": "Geri Y\u00f6nlendirme / NAT Modu", + "tunneling_type": "KNX T\u00fcnel Tipi" }, "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin." }, @@ -59,7 +60,8 @@ "host": "Sunucu", "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "port": "Port", - "route_back": "Geri Y\u00f6nlendirme / NAT Modu" + "route_back": "Geri Y\u00f6nlendirme / NAT Modu", + "tunneling_type": "KNX T\u00fcnel Tipi" } } } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index cd7322fc9e636..27b167c6551a5 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -14,7 +14,8 @@ "individual_address": "\u9023\u7dda\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u4fdd\u7559\u7a7a\u767d\u4ee5\u81ea\u52d5\u5075\u6e2c\uff09", "port": "\u901a\u8a0a\u57e0", - "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", + "tunneling_type": "KNX \u901a\u9053\u985e\u578b" }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -59,7 +60,8 @@ "host": "\u4e3b\u6a5f\u7aef", "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", - "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f" + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", + "tunneling_type": "KNX \u901a\u9053\u985e\u578b" } } } diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 1b0c5d521c9f5..73247d23a9df5 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,7 +1,9 @@ """Support for media browsing.""" import asyncio +import contextlib import logging +from homeassistant.components import media_source from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -184,7 +186,16 @@ async def item_payload(item, get_thumbnail_url=None): ) -async def library_payload(): +def media_source_content_filter(item: BrowseMedia) -> bool: + """Content filter for media sources.""" + # Filter out cameras using PNG over MJPEG. They don't work in Kodi. + return not ( + item.media_content_id.startswith("media-source://camera/") + and item.media_content_type == "image/png" + ) + + +async def library_payload(hass): """ Create response payload to describe contents of a specific library. @@ -222,6 +233,19 @@ async def library_payload(): ) ) + for child in library_info.children: + child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png" + + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_content_filter + ) + # If domain is None, it's overview of available sources + if item.domain is None: + library_info.children.extend(item.children) + else: + library_info.children.append(item) + return library_info diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 6e46b0883d904..86034ea9cfc61 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,9 +2,11 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", + "after_dependencies": ["media_source"], "requirements": ["pykodi==0.2.7"], "codeowners": ["@OnFreund", "@cgtobi"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["jsonrpc_async", "jsonrpc_base", "jsonrpc_websocket", "pykodi"] } diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 5067ee84826e8..53798a7ccd9f6 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -5,6 +5,7 @@ from functools import wraps import logging import re +from typing import Any import urllib.parse import jsonrpc_base @@ -12,7 +13,11 @@ from pykodi import CannotConnectError import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -24,6 +29,7 @@ MEDIA_TYPE_SEASON, MEDIA_TYPE_TRACK, MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, @@ -71,7 +77,12 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .browse_media import build_item_response, get_media_info, library_payload +from .browse_media import ( + build_item_response, + get_media_info, + library_payload, + media_source_content_filter, +) from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -691,8 +702,15 @@ async def async_media_seek(self, position): await self._kodi.media_seek(position) @cmd - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + media_type_lower = media_type.lower() if media_type_lower == MEDIA_TYPE_CHANNEL: @@ -700,7 +718,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "directory": - await self._kodi.play_directory(str(media_id)) + await self._kodi.play_directory(media_id) elif media_type_lower in [ MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, @@ -719,7 +737,9 @@ async def async_play_media(self, media_type, media_id, **kwargs): {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} ) else: - await self._kodi.play_file(str(media_id)) + media_id = async_process_play_media_url(self.hass, media_id) + + await self._kodi.play_file(media_id) @cmd async def async_set_shuffle(self, shuffle): @@ -898,7 +918,12 @@ async def _get_thumbnail_url( ) if media_content_type in [None, "library"]: - return await library_payload() + return await library_payload(self.hass) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_content_filter + ) payload = { "search_type": media_content_type, diff --git a/homeassistant/components/kodi/translations/el.json b/homeassistant/components/kodi/translations/el.json index 805490698bb9c..e0a3118ee05fa 100644 --- a/homeassistant/components/kodi/translations/el.json +++ b/homeassistant/components/kodi/translations/el.json @@ -1,8 +1,24 @@ { "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", + "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", + "no_uuid": "\u03a4\u03bf Kodi \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc. \u0391\u03c5\u03c4\u03cc \u03c0\u03b9\u03b8\u03b1\u03bd\u03cc\u03c4\u03b1\u03c4\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03bb\u03b9\u03ac \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Kodi (17.x \u03ae \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7). \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03ae \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Kodi.", + "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", + "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" + }, "flow_title": "Kodi: {\u03cc\u03bd\u03bf\u03bc\u03b1}", "step": { "credentials": { + "data": { + "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": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Kodi. \u0391\u03c5\u03c4\u03ac \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 / \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 / \u0394\u03af\u03ba\u03c4\u03c5\u03bf / \u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2." }, "discovery_confirm": { @@ -11,6 +27,8 @@ }, "user": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", "ssl": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 SSL" }, "description": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Kodi. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \"\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03bf\u03c5 Kodi \u03bc\u03ad\u03c3\u03c9 HTTP\" \u03c3\u03c4\u03bf \u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1/\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2/\u0394\u03af\u03ba\u03c4\u03c5\u03bf/\u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2." diff --git a/homeassistant/components/kodi/translations/pt-BR.json b/homeassistant/components/kodi/translations/pt-BR.json new file mode 100644 index 0000000000000..be8f6cbdfd30c --- /dev/null +++ b/homeassistant/components/kodi/translations/pt-BR.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_uuid": "A inst\u00e2ncia Kodi n\u00e3o possui um ID exclusivo. Isso provavelmente se deve a uma vers\u00e3o antiga do Kodi (17.x ou inferior). Voc\u00ea pode configurar a integra\u00e7\u00e3o manualmente ou atualizar para uma vers\u00e3o mais recente do Kodi.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "credentials": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Digite seu nome de usu\u00e1rio e senha Kodi. Eles podem ser encontrados em Sistema/Configura\u00e7\u00f5es/Rede/Servi\u00e7os." + }, + "discovery_confirm": { + "description": "Deseja adicionar Kodi (` {name} `) ao Home Assistant?", + "title": "Kodi descoberto" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta", + "ssl": "Usar um certificado SSL" + }, + "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de ativar \"Permitir controle do Kodi via HTTP\" em Sistema/Configura\u00e7\u00f5es/Rede/Servi\u00e7os." + }, + "ws_port": { + "data": { + "ws_port": "Porta" + }, + "description": "A porta WebSocket (\u00e0s vezes chamada de porta TCP no Kodi). Para se conectar pelo WebSocket, voc\u00ea precisa habilitar \"Permitir que programas ... controlem o Kodi\" em Sistema/Configura\u00e7\u00f5es/Rede/Servi\u00e7os. Se o WebSocket n\u00e3o estiver habilitado, remova a porta e deixe em branco." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} for solicitado para desligar", + "turn_on": "{entity_name} for solicitado para ativar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/sk.json b/homeassistant/components/kodi/translations/sk.json new file mode 100644 index 0000000000000..ab39cbe9c5e0f --- /dev/null +++ b/homeassistant/components/kodi/translations/sk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "credentials": { + "data": { + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "port": "Port" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index f018d1a51335b..9844076633468 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -281,7 +281,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_entry_updated(hass: HomeAssistant, entry: ConfigEntry): +async def async_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload the config entry when options change.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index ef878fc6f2bd5..af784750627a7 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -9,7 +9,7 @@ from .const import CONF_INVERSE, SIGNAL_DS18B20_NEW _LOGGER = logging.getLogger(__name__) -HANDLERS = decorator.Registry() +HANDLERS = decorator.Registry() # type: ignore[var-annotated] @HANDLERS.register("state") diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index c4ba720bc6ac2..93df24c850979 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -11,5 +11,6 @@ ], "dependencies": ["http"], "codeowners": ["@heythisisnate"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["konnected"] } diff --git a/homeassistant/components/konnected/translations/el.json b/homeassistant/components/konnected/translations/el.json index 0ce9aa09de5c0..b75982d16fa6b 100644 --- a/homeassistant/components/konnected/translations/el.json +++ b/homeassistant/components/konnected/translations/el.json @@ -1,9 +1,30 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "not_konn_panel": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Konnected.io", + "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" + }, "step": { + "confirm": { + "description": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf: {model}\n \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc: {id}\n \u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n \u0398\u03cd\u03c1\u03b1: {port} \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03c6\u03bf\u03c1\u03ac \u03c4\u03c9\u03bd IO \u03ba\u03b1\u03b9 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd Konnected.", + "title": "\u0388\u03c4\u03bf\u03b9\u03bc\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Konnected" + }, "import_confirm": { "description": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc {id} \u03c3\u03c4\u03bf configuration.yaml. \u0391\u03c5\u03c4\u03ae \u03b7 \u03c1\u03bf\u03ae \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03bf \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd.", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Konnected" + }, + "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03b3\u03b9\u03b1 \u03c4\u03bf Konnected Panel \u03c3\u03b1\u03c2." } } }, @@ -11,6 +32,9 @@ "abort": { "not_konn_panel": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Konnected.io" }, + "error": { + "bad_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 API" + }, "step": { "options_binary": { "data": { @@ -54,20 +78,31 @@ "alarm1": "ALARM1", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" - } + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03c9\u03bd I/O \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9. \u0398\u03b1 \u03bc\u03c0\u03bf\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03b5\u03c1\u03b5\u03af\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b1 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03ba\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03c9\u03bd \u03b5\u03b9\u03c3\u03cc\u03b4\u03c9\u03bd/\u03b5\u03be\u03cc\u03b4\u03c9\u03bd" }, "options_misc": { "data": { "api_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae API (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "blink": "\u0397 \u03bb\u03c5\u03c7\u03bd\u03af\u03b1 LED \u03c4\u03bf\u03c5 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03b5\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "discovery": "\u0391\u03bd\u03c4\u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03c3\u03b5 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2", "override_api_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 Home Assistant API" }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03b8\u03c5\u03bc\u03b7\u03c4\u03ae \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03c6\u03bf\u03c1\u03ac \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c3\u03b1\u03c2", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03c9\u03bd" }, "options_switch": { "data": { "activation": "\u0388\u03be\u03bf\u03b4\u03bf\u03c2 \u03cc\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", - "more_states": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03c9\u03bd \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7" - } + "momentary": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c0\u03b1\u03bb\u03bc\u03bf\u03cd (ms) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "more_states": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03c9\u03bd \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "pause": "\u03a0\u03b1\u03cd\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c0\u03b1\u03bb\u03bc\u03ce\u03bd (ms) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "repeat": "\u03a6\u03bf\u03c1\u03ad\u03c2 \u03b5\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7\u03c2 (-1=\u03ac\u03c0\u03b5\u03b9\u03c1\u03b5\u03c2) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + }, + "description": "{zone} : \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 {state}", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03c4\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03b5\u03be\u03cc\u03b4\u03bf\u03c5" } } } diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 78190aff5b361..81127f0dff2c6 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -81,8 +81,8 @@ "alarm2_out2": "OUT2 / ALARM2", "out1": "OUT1" }, - "description": "Selezionare di seguito la configurazione degli I/O rimanenti. Potrete configurare opzioni dettagliate nei prossimi passi.", - "title": "Configurazione I/O Esteso" + "description": "Selezionare di seguito la configurazione degli I/O rimanenti. Potrai configurare opzioni dettagliate nei prossimi passi.", + "title": "Configurazione I/O esteso" }, "options_misc": { "data": { diff --git a/homeassistant/components/konnected/translations/pt-BR.json b/homeassistant/components/konnected/translations/pt-BR.json index b31bd6feb8ae5..b49b487ab0dfb 100644 --- a/homeassistant/components/konnected/translations/pt-BR.json +++ b/homeassistant/components/konnected/translations/pt-BR.json @@ -1,7 +1,29 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "not_konn_panel": "N\u00e3o \u00e9 um dispositivo Konnected.io reconhecido", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, "step": { + "confirm": { + "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPorta: {port}\n\nVoc\u00ea pode configurar o comportamento do IO e do painel nas configura\u00e7\u00f5es do painel de alarme Konnected.", + "title": "Dispositivo Konnected pronto" + }, + "import_confirm": { + "description": "Um Painel de Alarmes Konnected com ID {id} foi descoberto em configuration.yaml. Esse fluxo permitir\u00e1 que voc\u00ea o importe para uma entrada de configura\u00e7\u00e3o.", + "title": "Importar dispositivo conectado" + }, "user": { + "data": { + "host": "Endere\u00e7o IP", + "port": "Porta" + }, "description": "Por favor, digite as informa\u00e7\u00f5es do host para o seu Painel Konnected." } } @@ -11,7 +33,7 @@ "not_konn_panel": "N\u00e3o \u00e9 um dispositivo Konnected.io reconhecido" }, "error": { - "bad_host": "URL de host da API de substitui\u00e7\u00e3o inv\u00e1lido" + "bad_host": "URL substituta para host da API inv\u00e1lido" }, "step": { "options_binary": { @@ -43,6 +65,7 @@ "7": "Zona 7", "out": "SA\u00cdDA" }, + "description": "Descobri um {model} em {host}. Selecione a configura\u00e7\u00e3o base de cada I/O abaixo - dependendo da I/O, pode permitir sensores bin\u00e1rios (contatos abertos/pr\u00f3ximos), sensores digitais (dht e ds18b20) ou sa\u00eddas comutadas. Voc\u00ea poder\u00e1 configurar op\u00e7\u00f5es detalhadas nos pr\u00f3ximos passos.", "title": "Configurar I/O" }, "options_io_ext": { @@ -51,14 +74,35 @@ "11": "Zona 11", "12": "Zona 12", "8": "Zona 8", - "9": "Zona 9" - } + "9": "Zona 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Selecione a configura\u00e7\u00e3o da I/O restante abaixo. Voc\u00ea poder\u00e1 configurar op\u00e7\u00f5es detalhadas nos pr\u00f3ximos passos.", + "title": "Configure I/O estendido" }, "options_misc": { "data": { "api_host": "Substituir URL do host da API (opcional)", + "blink": "LED do painel piscando ao enviar mudan\u00e7a de estado", + "discovery": "Responder \u00e0s solicita\u00e7\u00f5es de descoberta em sua rede", "override_api_host": "Substituir o URL padr\u00e3o do painel do host da API do Home Assistant" - } + }, + "description": "Selecione o comportamento desejado para o seu painel", + "title": "Configurar Misc" + }, + "options_switch": { + "data": { + "activation": "Sa\u00edda quando ligado", + "momentary": "Dura\u00e7\u00e3o do pulso (ms) (opcional)", + "more_states": "Configurar estados adicionais para esta zona", + "name": "Nome (opcional)", + "pause": "Pausa entre pulsos (ms) (opcional)", + "repeat": "Intervalo para repetir (-1=infinito) (opcional)" + }, + "description": "Selecione as op\u00e7\u00f5es para o switch conectado a {zone}: estado {state}", + "title": "Configurar o switch de sa\u00edda" } } } diff --git a/homeassistant/components/konnected/translations/sk.json b/homeassistant/components/konnected/translations/sk.json new file mode 100644 index 0000000000000..51eaba460d876 --- /dev/null +++ b/homeassistant/components/konnected/translations/sk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "options_binary": { + "data": { + "name": "N\u00e1zov (volite\u013en\u00fd)" + } + }, + "options_digital": { + "data": { + "name": "N\u00e1zov (volite\u013en\u00fd)" + } + }, + "options_switch": { + "data": { + "name": "N\u00e1zov (volite\u013en\u00fd)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/uk.json b/homeassistant/components/konnected/translations/uk.json index 92cd3744d945c..6a3170ffde733 100644 --- a/homeassistant/components/konnected/translations/uk.json +++ b/homeassistant/components/konnected/translations/uk.json @@ -64,7 +64,7 @@ "7": "\u0417\u043e\u043d\u0430 7", "out": "\u0412\u0418\u0425\u0406\u0414" }, - "description": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {model} \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {host}. \u0417\u0430\u043b\u0435\u0436\u043d\u043e \u0432\u0456\u0434 \u043e\u0431\u0440\u0430\u043d\u043e\u0457 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432, \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 \u043c\u043e\u0436\u0443\u0442\u044c \u0431\u0443\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0431\u0456\u043d\u0430\u0440\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0442\u044f / \u0437\u0430\u043a\u0440\u0438\u0442\u0442\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (dht \u0456 ds18b20) \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u0456 \u0432\u0438\u0445\u043e\u0434\u0438. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", + "description": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e {model} \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {host}. \u0417\u0430\u043b\u0435\u0436\u043d\u043e \u0432\u0456\u0434 \u043e\u0431\u0440\u0430\u043d\u043e\u0457 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432, \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 \u043c\u043e\u0436\u0443\u0442\u044c \u0431\u0443\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0431\u0456\u043d\u0430\u0440\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0442\u044f/\u0437\u0430\u043a\u0440\u0438\u0442\u0442\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (dht \u0456 ds18b20) \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u0456 \u0432\u0438\u0445\u043e\u0434\u0438. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432" }, "options_io_ext": { diff --git a/homeassistant/components/kostal_plenticore/diagnostics.py b/homeassistant/components/kostal_plenticore/diagnostics.py new file mode 100644 index 0000000000000..2e061d35528a4 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/diagnostics.py @@ -0,0 +1,42 @@ +"""Diagnostics support for Kostal Plenticore.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import REDACTED, async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .helper import Plenticore + +TO_REDACT = {CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)} + + plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id] + + # Get information from Kostal Plenticore library + available_process_data = await plenticore.client.get_process_data() + available_settings_data = await plenticore.client.get_settings() + data["client"] = { + "version": str(await plenticore.client.get_version()), + "me": str(await plenticore.client.get_me()), + "available_process_data": available_process_data, + "available_settings_data": { + module_id: [str(setting) for setting in settings] + for module_id, settings in available_settings_data.items() + }, + } + + device_info = {**plenticore.device_info} + device_info["identifiers"] = REDACTED # contains serial number + data["device"] = device_info + + return data diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 6dd72412fd8db..e047c0dafba42 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -123,9 +123,7 @@ class DataUpdateCoordinatorMixin: async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: """Write settings back to Plenticore.""" - client = self._plenticore.client - - if client is None: + if (client := self._plenticore.client) is None: return False try: @@ -137,9 +135,7 @@ async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool] async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: """Write settings back to Plenticore.""" - client = self._plenticore.client - - if client is None: + if (client := self._plenticore.client) is None: return False try: @@ -272,9 +268,7 @@ class SelectDataUpdateCoordinator( """Implementation of PlenticoreUpdateCoordinator for select data.""" async def _async_update_data(self) -> dict[str, dict[str, str]]: - client = self._plenticore.client - - if client is None: + if self._plenticore.client is None: return {} _LOGGER.debug("Fetching select %s for %s", self.name, self._fetch) diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json index 9e6d4353259fb..71f71cae993fb 100644 --- a/homeassistant/components/kostal_plenticore/manifest.json +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", "requirements": ["kostal_plenticore==0.2.0"], "codeowners": ["@stegm"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["kostal"] } diff --git a/homeassistant/components/kostal_plenticore/translations/el.json b/homeassistant/components/kostal_plenticore/translations/el.json index 48edc219315ea..518070a76ef12 100644 --- a/homeassistant/components/kostal_plenticore/translations/el.json +++ b/homeassistant/components/kostal_plenticore/translations/el.json @@ -1,3 +1,21 @@ { + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "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" + } + } + } + }, "title": "\u0397\u03bb\u03b9\u03b1\u03ba\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 Kostal Plenticore" } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/pt-BR.json b/homeassistant/components/kostal_plenticore/translations/pt-BR.json new file mode 100644 index 0000000000000..a670c5a41be32 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha" + } + } + } + }, + "title": "Inversor Solar Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/sk.json b/homeassistant/components/kostal_plenticore/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/manifest.json b/homeassistant/components/kraken/manifest.json index c7d1ca4d0ed8b..8cbc29f52bd35 100644 --- a/homeassistant/components/kraken/manifest.json +++ b/homeassistant/components/kraken/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/kraken", "requirements": ["krakenex==2.1.0", "pykrakenapi==0.1.8"], "codeowners": ["@eifinger"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["krakenex", "pykrakenapi"] } diff --git a/homeassistant/components/kraken/translations/el.json b/homeassistant/components/kraken/translations/el.json new file mode 100644 index 0000000000000..252d1a5ebd2a8 --- /dev/null +++ b/homeassistant/components/kraken/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\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." + }, + "step": { + "user": { + "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;" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", + "tracked_asset_pairs": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03b6\u03b5\u03cd\u03b3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c5\u03c3\u03b9\u03b1\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/pt-BR.json b/homeassistant/components/kraken/translations/pt-BR.json new file mode 100644 index 0000000000000..955386f20982b --- /dev/null +++ b/homeassistant/components/kraken/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o", + "tracked_asset_pairs": "Pares de ativos rastreados" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/zh-Hant.json b/homeassistant/components/kraken/translations/zh-Hant.json index 8d64c0265793a..53b8a1fa23677 100644 --- a/homeassistant/components/kraken/translations/zh-Hant.json +++ b/homeassistant/components/kraken/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "already_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index f56688e919ac0..1f9c67b9aa1ff 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -3,6 +3,7 @@ import pykulersky +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DOMAIN @@ -10,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) -async def _async_has_devices(hass) -> bool: +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" # Check if there are any devices that can be discovered in the network. try: diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json index 24091ec65c809..581fe53424bfb 100644 --- a/homeassistant/components/kulersky/manifest.json +++ b/homeassistant/components/kulersky/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/kulersky", "requirements": ["pykulersky==0.5.2"], "codeowners": ["@emlove"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bleak", "pykulersky"] } diff --git a/homeassistant/components/kulersky/translations/el.json b/homeassistant/components/kulersky/translations/el.json new file mode 100644 index 0000000000000..a13912159002b --- /dev/null +++ b/homeassistant/components/kulersky/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/pt-BR.json b/homeassistant/components/kulersky/translations/pt-BR.json new file mode 100644 index 0000000000000..1778d39a7d082 --- /dev/null +++ b/homeassistant/components/kulersky/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/uk.json b/homeassistant/components/kulersky/translations/uk.json index 292861e9129db..5c2489c2a18ab 100644 --- a/homeassistant/components/kulersky/translations/uk.json +++ b/homeassistant/components/kulersky/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/kulersky/translations/zh-Hant.json b/homeassistant/components/kulersky/translations/zh-Hant.json index 90c98e491dfea..cfd20d603cba1 100644 --- a/homeassistant/components/kulersky/translations/zh-Hant.json +++ b/homeassistant/components/kulersky/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index b84d36131e5f3..b5229f7a0fefa 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": ["pykwb==0.0.8"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pykwb"] } diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 922c0e9d17361..c377d29d2a0f4 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": ["pylacrosse==0.4"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pylacrosse"] } diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 1c84c4f551047..970bdd4b3b69c 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,6 +1,4 @@ """Support for LaMetric time.""" -import logging - from lmnotify import LaMetricManager import voluptuous as vol @@ -9,12 +7,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - - -DOMAIN = "lametric" - -LAMETRIC_DEVICES = "LAMETRIC_DEVICES" +from .const import DOMAIN, LOGGER CONFIG_SCHEMA = vol.Schema( { @@ -31,18 +24,18 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LaMetricManager.""" - _LOGGER.debug("Setting up LaMetric platform") + LOGGER.debug("Setting up LaMetric platform") conf = config[DOMAIN] hlmn = HassLaMetricManager( client_id=conf[CONF_CLIENT_ID], client_secret=conf[CONF_CLIENT_SECRET] ) if not (devices := hlmn.manager.get_devices()): - _LOGGER.error("No LaMetric devices found") + LOGGER.error("No LaMetric devices found") return False hass.data[DOMAIN] = hlmn for dev in devices: - _LOGGER.debug("Discovered LaMetric device: %s", dev) + LOGGER.debug("Discovered LaMetric device: %s", dev) return True @@ -53,7 +46,7 @@ class HassLaMetricManager: def __init__(self, client_id: str, client_secret: str) -> None: """Initialize HassLaMetricManager and connect to LaMetric.""" - _LOGGER.debug("Connecting to LaMetric") + LOGGER.debug("Connecting to LaMetric") self.manager = LaMetricManager(client_id, client_secret) self._client_id = client_id self._client_secret = client_secret diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py new file mode 100644 index 0000000000000..85e61cd8d9ae3 --- /dev/null +++ b/homeassistant/components/lametric/const.py @@ -0,0 +1,16 @@ +"""Constants for the LaMetric integration.""" + +import logging +from typing import Final + +DOMAIN: Final = "lametric" + +LOGGER = logging.getLogger(__package__) + +AVAILABLE_PRIORITIES: Final = ["info", "warning", "critical"] +AVAILABLE_ICON_TYPES: Final = ["none", "info", "alert"] + +CONF_CYCLES: Final = "cycles" +CONF_LIFETIME: Final = "lifetime" +CONF_PRIORITY: Final = "priority" +CONF_ICON_TYPE: Final = "icon_type" diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index a27ab3a48d9ad..a2c0aecb58dc8 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["lmnotify==0.0.4"], "codeowners": ["@robbiet480", "@frenck"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["lmnotify"] } diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 034deb911d164..f3c098a841e6a 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -1,7 +1,6 @@ """Support for LaMetric notifications.""" from __future__ import annotations -import logging from typing import Any from lmnotify import Model, SimpleFrame, Sound @@ -20,17 +19,17 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, HassLaMetricManager - -_LOGGER = logging.getLogger(__name__) - -AVAILABLE_PRIORITIES = ["info", "warning", "critical"] -AVAILABLE_ICON_TYPES = ["none", "info", "alert"] - -CONF_CYCLES = "cycles" -CONF_LIFETIME = "lifetime" -CONF_PRIORITY = "priority" -CONF_ICON_TYPE = "icon_type" +from . import HassLaMetricManager +from .const import ( + AVAILABLE_ICON_TYPES, + AVAILABLE_PRIORITIES, + CONF_CYCLES, + CONF_ICON_TYPE, + CONF_LIFETIME, + CONF_PRIORITY, + DOMAIN, + LOGGER, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -85,7 +84,7 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: targets = kwargs.get(ATTR_TARGET) data = kwargs.get(ATTR_DATA) - _LOGGER.debug("Targets/Data: %s/%s", targets, data) + LOGGER.debug("Targets/Data: %s/%s", targets, data) icon = self._icon cycles = self._cycles sound = None @@ -99,16 +98,16 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: if "sound" in data: try: sound = Sound(category="notifications", sound_id=data["sound"]) - _LOGGER.debug("Adding notification sound %s", data["sound"]) + LOGGER.debug("Adding notification sound %s", data["sound"]) except AssertionError: - _LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) + LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) if "cycles" in data: cycles = int(data["cycles"]) if "icon_type" in data: if data["icon_type"] in AVAILABLE_ICON_TYPES: icon_type = data["icon_type"] else: - _LOGGER.warning( + LOGGER.warning( "Priority %s invalid, using default %s", data["priority"], priority, @@ -117,13 +116,13 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: if data["priority"] in AVAILABLE_PRIORITIES: priority = data["priority"] else: - _LOGGER.warning( + LOGGER.warning( "Priority %s invalid, using default %s", data["priority"], priority, ) text_frame = SimpleFrame(icon, message) - _LOGGER.debug( + LOGGER.debug( "Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", icon, message, @@ -138,11 +137,11 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: try: self._devices = lmn.get_devices() except TokenExpiredError: - _LOGGER.debug("Token expired, fetching new token") + LOGGER.debug("Token expired, fetching new token") lmn.get_token() self._devices = lmn.get_devices() except RequestsConnectionError: - _LOGGER.warning( + LOGGER.warning( "Problem connecting to LaMetric, using cached devices instead" ) for dev in self._devices: @@ -155,6 +154,6 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: priority=priority, icon_type=icon_type, ) - _LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) + LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) except OSError: - _LOGGER.warning("Cannot connect to LaMetric %s", dev["name"]) + LOGGER.warning("Cannot connect to LaMetric %s", dev["name"]) diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index f850b39a6204d..3c8aef9f67393 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": ["pylast==4.2.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pylast"] } diff --git a/homeassistant/components/launch_library/translations/el.json b/homeassistant/components/launch_library/translations/el.json index 8a25e8b76a05e..757cb3baee9d9 100644 --- a/homeassistant/components/launch_library/translations/el.json +++ b/homeassistant/components/launch_library/translations/el.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "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." + }, "step": { "user": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u0392\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2;" diff --git a/homeassistant/components/launch_library/translations/fr.json b/homeassistant/components/launch_library/translations/fr.json index 57d3902c1f02e..ba346a19acd9a 100644 --- a/homeassistant/components/launch_library/translations/fr.json +++ b/homeassistant/components/launch_library/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, "step": { "user": { "description": "Voulez-vous configurer la biblioth\u00e8que de lancement\u00a0?" diff --git a/homeassistant/components/launch_library/translations/id.json b/homeassistant/components/launch_library/translations/id.json index 3a870e47986bf..a6b60231a85df 100644 --- a/homeassistant/components/launch_library/translations/id.json +++ b/homeassistant/components/launch_library/translations/id.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin mengonfigurasi Launch Library?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/launch_library/translations/pt-BR.json b/homeassistant/components/launch_library/translations/pt-BR.json new file mode 100644 index 0000000000000..553d0dd761d1b --- /dev/null +++ b/homeassistant/components/launch_library/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja configurar a Biblioteca de Lan\u00e7amento?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/launch_library/translations/zh-Hant.json b/homeassistant/components/launch_library/translations/zh-Hant.json index b7fb63e939a5b..23bf571cc4b5d 100644 --- a/homeassistant/components/launch_library/translations/zh-Hant.json +++ b/homeassistant/components/launch_library/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 9316d4309c917..924ff5b278cd3 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import pypck @@ -16,7 +17,6 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN @@ -24,7 +24,7 @@ def get_config_entry( - hass: HomeAssistant, data: ConfigType + hass: HomeAssistant, data: dict[str, Any] ) -> config_entries.ConfigEntry | None: """Check config entries for already configured entries based on the ip address/port.""" return next( @@ -38,7 +38,7 @@ def get_config_entry( ) -async def validate_connection(host_name: str, data: ConfigType) -> ConfigType: +async def validate_connection(host_name: str, data: dict[str, Any]) -> dict[str, Any]: """Validate if a connection to LCN can be established.""" host = data[CONF_IP_ADDRESS] port = data[CONF_PORT] @@ -70,7 +70,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, data: ConfigType) -> FlowResult: + async def async_step_import(self, data: dict[str, Any]) -> FlowResult: """Import existing configuration from LCN.""" host_name = data[CONF_HOST] # validate the imported connection parameters diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index b82724f05d6a2..35575a442b201 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -56,8 +56,7 @@ async def async_get_triggers( ) -> list[dict[str, Any]]: """List device triggers for LCN devices.""" device_registry = dr.async_get(hass) - device = device_registry.async_get(device_id) - if device is None: + if (device := device_registry.async_get(device_id)) is None: return [] identifier = next(iter(device.identifiers)) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 87624a4e4aef6..412ef74e3b82c 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,8 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.13"], + "requirements": ["pypck==0.7.14"], "codeowners": ["@alengwenus"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pypck"] } diff --git a/homeassistant/components/lcn/translations/de.json b/homeassistant/components/lcn/translations/de.json index e7716b1bebae5..b4a731fc1f62b 100644 --- a/homeassistant/components/lcn/translations/de.json +++ b/homeassistant/components/lcn/translations/de.json @@ -2,7 +2,7 @@ "device_automation": { "trigger_type": { "fingerprint": "Fingerabdruckcode empfangen", - "send_keys": "Sendeschl\u00fcssel empfangen", + "send_keys": "Sende Tasten empfangen", "transmitter": "Sendercode empfangen", "transponder": "Transpondercode empfangen" } diff --git a/homeassistant/components/lcn/translations/el.json b/homeassistant/components/lcn/translations/el.json new file mode 100644 index 0000000000000..ae71f96d3617f --- /dev/null +++ b/homeassistant/components/lcn/translations/el.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b1\u03ba\u03c4\u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03b1\u03c0\u03bf\u03c4\u03c5\u03c0\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", + "send_keys": "\u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ce\u03bd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", + "transmitter": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03cd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", + "transponder": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/pt-BR.json b/homeassistant/components/lcn/translations/pt-BR.json new file mode 100644 index 0000000000000..9898533ea72a6 --- /dev/null +++ b/homeassistant/components/lcn/translations/pt-BR.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "fingerprint": "c\u00f3digo de impress\u00e3o digital recebido", + "send_keys": "enviar chaves recebidas", + "transmitter": "c\u00f3digo do transmissor recebido", + "transponder": "c\u00f3digo do transponder recebido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 18f296e1c53cd..5006b88a40728 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": ["pylgnetcast==0.3.7"], "codeowners": ["@Drafteed"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pylgnetcast"] } diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 64ef4f0939fe1..5faf6941aeb4d 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -20,6 +20,7 @@ SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( @@ -48,6 +49,7 @@ SUPPORT_LGTV = ( SUPPORT_PAUSE | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK @@ -121,11 +123,8 @@ def update(self): try: with self._client as client: self._state = STATE_PLAYING - volume_info = client.query_data("volume_info") - if volume_info: - volume_info = volume_info[0] - self._volume = float(volume_info.find("level").text) - self._muted = volume_info.find("mute").text == "true" + + self.__update_volume() channel_info = client.query_data("cur_channel") if channel_info: @@ -160,6 +159,13 @@ def update(self): except (LgNetCastError, RequestException): self._state = STATE_OFF + def __update_volume(self): + volume_info = self._client.get_volume() + if volume_info: + (volume, muted) = volume_info + self._volume = volume + self._muted = muted + @property def name(self): """Return the name of the device.""" @@ -241,6 +247,10 @@ def volume_down(self): """Volume down media player.""" self.send_command(25) + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._client.set_volume(float(volume * 100)) + def mute_volume(self, mute): """Send mute command.""" self.send_command(26) diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index 671b1d2ca5735..f40ad1d194cbb 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": ["temescal==0.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["temescal"] } diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 54919088262b7..23fdad892d25f 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], "requirements": ["life360==4.1.1"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["life360"] } diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index 14fc1ef094f2d..07106d89d63c4 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -1,13 +1,24 @@ { "config": { + "abort": { + "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" + }, "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})." }, "error": { - "invalid_username": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "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", + "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": { "user": { + "data": { + "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/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 5894376a065e4..7753c0f84dcc9 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "create_entry": { "default": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url})." }, "error": { - "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido" + "already_configured": "A conta j\u00e1 foi configurada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/sk.json b/homeassistant/components/life360/translations/sk.json new file mode 100644 index 0000000000000..2c3ed1dd93049 --- /dev/null +++ b/homeassistant/components/life360/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 1713683b720d3..c48bee9e4e724 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,12 +1,13 @@ """Config flow flow LIFX.""" import aiolifx +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DOMAIN -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() return len(lifx_ip_addresses) > 0 diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index f921dabdf4a2d..a7c52424d979c 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -51,6 +51,7 @@ import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util @@ -199,9 +200,12 @@ async def async_setup_entry( interfaces = [{}] platform = entity_platform.async_get_current_platform() - lifx_manager = LIFXManager(hass, platform, async_add_entities) + lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) hass.data[DATA_LIFX_MANAGER] = lifx_manager + # This is to clean up old litter. Can be removed in Home Assistant 2022.5. + await lifx_manager.remove_empty_devices() + for interface in interfaces: lifx_manager.start_discovery(interface) @@ -254,17 +258,21 @@ def merge_hsbk(base, change): class LIFXManager: """Representation of all known LIFX entities.""" - def __init__(self, hass, platform, async_add_entities): + def __init__(self, hass, platform, config_entry, async_add_entities): """Initialize the light.""" self.entities = {} self.hass = hass self.platform = platform + self.config_entry = config_entry self.async_add_entities = async_add_entities self.effects_conductor = aiolifx_effects().Conductor(hass.loop) self.discoveries = [] self.cleanup_unsub = self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STOP, self.cleanup ) + self.entity_registry_updated_unsub = self.hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, self.entity_registry_updated + ) self.register_set_state() self.register_effects() @@ -289,6 +297,7 @@ def start_discovery(self, interface): def cleanup(self, event=None): """Release resources.""" self.cleanup_unsub() + self.entity_registry_updated_unsub() for discovery in self.discoveries: discovery.cleanup() @@ -424,6 +433,26 @@ def unregister(self, bulb): entity.registered = False entity.async_write_ha_state() + async def entity_registry_updated(self, event): + """Handle entity registry updated.""" + if event.data["action"] == "remove": + await self.remove_empty_devices() + + async def remove_empty_devices(self): + """Remove devices with no entities.""" + entity_reg = await er.async_get_registry(self.hass) + device_reg = dr.async_get(self.hass) + device_list = dr.async_entries_for_config_entry( + device_reg, self.config_entry.entry_id + ) + for device_entry in device_list: + if not er.async_entries_for_device( + entity_reg, + device_entry.id, + include_disabled_entities=True, + ): + device_reg.async_remove_device(device_entry.id) + class AwaitAioLIFX: """Wait for an aiolifx callback and return the message.""" diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 2dc46615f3a1e..b034745ee3164 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -8,5 +8,6 @@ "models": ["LIFX"] }, "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiolifx", "aiolifx_effects", "bitstring"] } diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json new file mode 100644 index 0000000000000..5a1d7707f5586 --- /dev/null +++ b/homeassistant/components/lifx/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf LIFX;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index cf374894623fe..f67284d8b5d3b 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 poss\u00edvel." + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json index 8c32e79533dc0..556729e895b84 100644 --- a/homeassistant/components/lifx/translations/uk.json +++ b/homeassistant/components/lifx/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index 154e82ec3011e..911eaa570d1c6 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 9c382fcb7fac2..d60e0a10f3a63 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging -from types import MappingProxyType from typing import Any, NamedTuple, cast from homeassistant.const import ( @@ -213,8 +212,6 @@ async def async_reproduce_states( ) -def check_attr_equal( - attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str -) -> bool: +def check_attr_equal(attr1: Mapping, attr2: Mapping, attr_str: str) -> bool: """Return true if the given attributes are equal.""" return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/homeassistant/components/light/translations/el.json b/homeassistant/components/light/translations/el.json index d3ec06dcbfbdb..4b5c69c61018b 100644 --- a/homeassistant/components/light/translations/el.json +++ b/homeassistant/components/light/translations/el.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "\u039c\u03b5\u03af\u03c9\u03c3\u03b7 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 {entity_name}", "brightness_increase": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 {entity_name}", + "flash": "\u03a6\u03bb\u03b1\u03c2 {entity_name}", "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", "turn_off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}", "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}" diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 1eb2914d110f3..10e8dfa3d17f7 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { + "changed_states": "{entity_name} activado o desactivado", "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} apagada", "turned_on": "{entity_name} encendida" diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index 1976c7b8fd4f1..7863f5ad5ebb1 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { + "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9" diff --git a/homeassistant/components/light/translations/id.json b/homeassistant/components/light/translations/id.json index 25c636ac1c7e5..334e938d41e9e 100644 --- a/homeassistant/components/light/translations/id.json +++ b/homeassistant/components/light/translations/id.json @@ -13,6 +13,8 @@ "is_on": "{entity_name} nyala" }, "trigger_type": { + "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", + "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/light/translations/nl.json b/homeassistant/components/light/translations/nl.json index 190ed3f52bdc9..f70830601c7ae 100644 --- a/homeassistant/components/light/translations/nl.json +++ b/homeassistant/components/light/translations/nl.json @@ -13,6 +13,8 @@ "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { + "changed_states": "{entity_name} in- of uitgeschakeld", + "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} is uitgeschakeld", "turned_on": "{entity_name} is ingeschakeld" } diff --git a/homeassistant/components/light/translations/pl.json b/homeassistant/components/light/translations/pl.json index c1a3a0a8084cd..375cf8a8ce3f2 100644 --- a/homeassistant/components/light/translations/pl.json +++ b/homeassistant/components/light/translations/pl.json @@ -13,6 +13,7 @@ "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { + "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" diff --git a/homeassistant/components/light/translations/pt-BR.json b/homeassistant/components/light/translations/pt-BR.json index 27b9b46297a22..f884baf9b3ced 100644 --- a/homeassistant/components/light/translations/pt-BR.json +++ b/homeassistant/components/light/translations/pt-BR.json @@ -1,17 +1,22 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Diminuir o brilho {entity_name}", + "brightness_increase": "Aumente o brilho {entity_name}", + "flash": "Flash {entity_name}", "toggle": "Alternar {entity_name}", "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e1 desligado", - "is_on": "{entity_name} est\u00e1 ligado" + "is_off": "{entity_name} est\u00e1 desligada", + "is_on": "{entity_name} est\u00e1 ligada" }, "trigger_type": { - "turned_off": "{entity_name} desligado", - "turned_on": "{entity_name} ligado" + "changed_states": "{entity_name} for ligada ou desligada", + "toggled": "{entity_name} for ligada ou desligada", + "turned_off": "{entity_name} for desligada", + "turned_on": "{entity_name} for ligada" } }, "state": { diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index d77075a0c564a..746d702b68973 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": ["lightwave==0.20"], "codeowners": [], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["lightwave"] } diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index f0a8888214a7a..bf6f00d66ad40 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": ["limitlessled==1.1.3"], "codeowners": [], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["limitlessled"] } diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index 2732535455364..df600e357aa94 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": ["linode-api==4.1.9b1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["linode"] } diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 4502bd039f407..a35f77525622c 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": ["batinfo==0.4.2"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["batinfo"] } diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index 3e688bdef6fd0..e497927180a34 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": ["python-lirc==1.2.3"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["lirc"] } diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 7481cabb65539..c6e958d3a1077 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pylitejet==0.3.0"], "codeowners": ["@joncar"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pylitejet"] } diff --git a/homeassistant/components/litejet/translations/el.json b/homeassistant/components/litejet/translations/el.json index 9724aa87b12ba..49112fc0c20cb 100644 --- a/homeassistant/components/litejet/translations/el.json +++ b/homeassistant/components/litejet/translations/el.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "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": { + "open_failed": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03c4\u03bf \u03ac\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2." + }, "step": { "user": { + "data": { + "port": "\u0398\u03cd\u03c1\u03b1" + }, "description": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 RS232-2 \u03c4\u03bf\u03c5 LiteJet \u03c3\u03c4\u03bf\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2.\n\n\u03a4\u03bf LiteJet MCP \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b3\u03b9\u03b1 19,2 K baud, 8 bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd, 1 stop bit, \u03c7\u03c9\u03c1\u03af\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b1 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03b5\u03b9 \u03ad\u03bd\u03b1 'CR' \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03ba\u03ac\u03b8\u03b5 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 LiteJet" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/pt-BR.json b/homeassistant/components/litejet/translations/pt-BR.json new file mode 100644 index 0000000000000..e41fc57329e71 --- /dev/null +++ b/homeassistant/components/litejet/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "open_failed": "N\u00e3o \u00e9 poss\u00edvel abrir a porta serial especificada." + }, + "step": { + "user": { + "data": { + "port": "Porta" + }, + "description": "Conecte a porta RS232-2 do LiteJet ao seu computador e digite o caminho para o dispositivo de porta serial. \n\n O LiteJet MCP deve ser configurado para 19,2 K baud, 8 bits de dados, 1 bit de parada, sem paridade e para transmitir um 'CR' ap\u00f3s cada resposta.", + "title": "Conecte-se ao LiteJet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Transi\u00e7\u00e3o padr\u00e3o (segundos)" + }, + "title": "Configurar LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/sk.json b/homeassistant/components/litejet/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/litejet/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/zh-Hant.json b/homeassistant/components/litejet/translations/zh-Hant.json index 3e6886e74a32f..dc7747a3ddec5 100644 --- a/homeassistant/components/litejet/translations/zh-Hant.json +++ b/homeassistant/components/litejet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "open_failed": "\u7121\u6cd5\u958b\u555f\u6307\u5b9a\u7684\u5e8f\u5217\u57e0" diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 2c4ed37f955d7..51b88bb4f7924 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -9,7 +9,7 @@ from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -60,7 +60,7 @@ class LitterRobotControlEntity(LitterRobotEntity): def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) - self._refresh_callback = None + self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( self, action: MethodType, *args: Any, **kwargs: Any @@ -99,8 +99,11 @@ def async_cancel_refresh_callback(self): self._refresh_callback = None @staticmethod - def parse_time_at_default_timezone(time_str: str) -> time | None: + def parse_time_at_default_timezone(time_str: str | None) -> time | None: """Parse a time string and add default timezone.""" + if time_str is None: + return None + if (parsed_time := dt_util.parse_time(time_str)) is None: return None @@ -127,7 +130,7 @@ def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: async def perform_action_and_assume_state( self, action: MethodType, assumed_state: Any - ) -> bool: + ) -> None: """Perform an action and assume the state passed in if call is successful.""" if await self.perform_action_and_refresh(action, assumed_state): self._assumed_state = assumed_state diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 3aa86dcc93ac1..43d60e534eae9 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,4 +1,7 @@ """A wrapper 'hub' for the Litter-Robot API.""" +from __future__ import annotations + +from collections.abc import Mapping from datetime import timedelta import logging @@ -19,11 +22,11 @@ class LitterRobotHub: """A Litter-Robot hub wrapper class.""" - def __init__(self, hass: HomeAssistant, data: dict) -> None: + account: Account + + def __init__(self, hass: HomeAssistant, data: Mapping) -> None: """Initialize the Litter-Robot hub.""" self._data = data - self.account = None - self.logged_in = False async def _async_update_data() -> bool: """Update all device states from the Litter-Robot API.""" @@ -40,7 +43,6 @@ async def _async_update_data() -> bool: async def login(self, load_robots: bool = False) -> None: """Login to Litter-Robot.""" - self.logged_in = False self.account = Account() try: await self.account.connect( @@ -48,8 +50,7 @@ async def login(self, load_robots: bool = False) -> None: password=self._data[CONF_PASSWORD], load_robots=load_robots, ) - self.logged_in = True - return self.logged_in + return except LitterRobotLoginException as ex: _LOGGER.error("Invalid credentials") raise ex diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ab05ab111f01c..b404762fbf37d 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@natekspencer" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pylitterbot"] } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/el.json b/homeassistant/components/litterrobot/translations/el.json new file mode 100644 index 0000000000000..cdc7ae85736f1 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/el.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/nb.json b/homeassistant/components/litterrobot/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pt-BR.json b/homeassistant/components/litterrobot/translations/pt-BR.json new file mode 100644 index 0000000000000..d86aef5d51d73 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sk.json b/homeassistant/components/litterrobot/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/el.json b/homeassistant/components/local_ip/translations/el.json index 8024f3e962d4d..c33fa936300ca 100644 --- a/homeassistant/components/local_ip/translations/el.json +++ b/homeassistant/components/local_ip/translations/el.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "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." + }, "step": { "user": { + "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;", "title": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" } } diff --git a/homeassistant/components/local_ip/translations/pt-BR.json b/homeassistant/components/local_ip/translations/pt-BR.json index 179e720abcaff..14c377783f4b8 100644 --- a/homeassistant/components/local_ip/translations/pt-BR.json +++ b/homeassistant/components/local_ip/translations/pt-BR.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o do IP local \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?", "title": "Endere\u00e7o IP local" } } diff --git a/homeassistant/components/local_ip/translations/uk.json b/homeassistant/components/local_ip/translations/uk.json index 52aed47fa2076..d8ec556180f83 100644 --- a/homeassistant/components/local_ip/translations/uk.json +++ b/homeassistant/components/local_ip/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { "user": { diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json index d7498843b7537..d88dbf235d8ea 100644 --- a/homeassistant/components/local_ip/translations/zh-Hant.json +++ b/homeassistant/components/local_ip/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/bg.json b/homeassistant/components/locative/translations/bg.json index 9c79f86d4f76d..2daf1d17c8001 100644 --- a/homeassistant/components/locative/translations/bg.json +++ b/homeassistant/components/locative/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/locative/translations/ca.json b/homeassistant/components/locative/translations/ca.json index 637b937a568a5..b6ec5bd111149 100644 --- a/homeassistant/components/locative/translations/ca.json +++ b/homeassistant/components/locative/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json index 5ca0036347618..801ec910745dd 100644 --- a/homeassistant/components/locative/translations/de.json +++ b/homeassistant/components/locative/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/locative/translations/el.json b/homeassistant/components/locative/translations/el.json index e041741e99298..0bfb57d149f3a 100644 --- a/homeassistant/components/locative/translations/el.json +++ b/homeassistant/components/locative/translations/el.json @@ -1,10 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Locative.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: \n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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." + }, + "step": { + "user": { + "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;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Locative Webhook" + } } } } \ No newline at end of file diff --git a/homeassistant/components/locative/translations/en.json b/homeassistant/components/locative/translations/en.json index 760835c8ea8dc..9171029375136 100644 --- a/homeassistant/components/locative/translations/en.json +++ b/homeassistant/components/locative/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/locative/translations/et.json b/homeassistant/components/locative/translations/et.json index e73ad4da42050..fb1a65ced8e95 100644 --- a/homeassistant/components/locative/translations/et.json +++ b/homeassistant/components/locative/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/locative/translations/fr.json b/homeassistant/components/locative/translations/fr.json index 9c9414caf9ba5..45a6f6fe5943a 100644 --- a/homeassistant/components/locative/translations/fr.json +++ b/homeassistant/components/locative/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/locative/translations/he.json b/homeassistant/components/locative/translations/he.json index 7e155c6bdd7a2..4850982aede2c 100644 --- a/homeassistant/components/locative/translations/he.json +++ b/homeassistant/components/locative/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." }, diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index 893e22f147126..b74774c3c21ef 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/locative/translations/id.json b/homeassistant/components/locative/translations/id.json index 71aea5cc63cff..89e61d009e9d5 100644 --- a/homeassistant/components/locative/translations/id.json +++ b/homeassistant/components/locative/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/locative/translations/it.json b/homeassistant/components/locative/translations/it.json index a9215ea6553cd..7cdb9b2170d3e 100644 --- a/homeassistant/components/locative/translations/it.json +++ b/homeassistant/components/locative/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index 89003e78a9dbe..a4f03bde29f8d 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/locative/translations/nb.json b/homeassistant/components/locative/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/locative/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json index ed39d00430b1d..0a459e566c56d 100644 --- a/homeassistant/components/locative/translations/nl.json +++ b/homeassistant/components/locative/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/locative/translations/no.json b/homeassistant/components/locative/translations/no.json index 7eca10016eaff..0982c62752654 100644 --- a/homeassistant/components/locative/translations/no.json +++ b/homeassistant/components/locative/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/locative/translations/pl.json b/homeassistant/components/locative/translations/pl.json index f91afa32f74a6..2eeb40aee6b66 100644 --- a/homeassistant/components/locative/translations/pl.json +++ b/homeassistant/components/locative/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/locative/translations/pt-BR.json b/homeassistant/components/locative/translations/pt-BR.json index 20bcaaad64312..d134a5113f442 100644 --- a/homeassistant/components/locative/translations/pt-BR.json +++ b/homeassistant/components/locative/translations/pt-BR.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar locais para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso webhook no aplicativo Locative. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais detalhes." }, "step": { "user": { - "description": "Tem certeza de que deseja configurar o Locative Webhook?", + "description": "Deseja iniciar a configura\u00e7\u00e3o?", "title": "Configurar o Locative Webhook" } } diff --git a/homeassistant/components/locative/translations/ru.json b/homeassistant/components/locative/translations/ru.json index dd353c25117da..90a27fa6e51c1 100644 --- a/homeassistant/components/locative/translations/ru.json +++ b/homeassistant/components/locative/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json index 906abd1b2e55d..e48ff5d9b56e9 100644 --- a/homeassistant/components/locative/translations/tr.json +++ b/homeassistant/components/locative/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/locative/translations/uk.json b/homeassistant/components/locative/translations/uk.json index d9a4713087117..ccfac69f0a906 100644 --- a/homeassistant/components/locative/translations/uk.json +++ b/homeassistant/components/locative/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/locative/translations/zh-Hans.json b/homeassistant/components/locative/translations/zh-Hans.json index 00eaf2929ddb3..a96698dedacd6 100644 --- a/homeassistant/components/locative/translations/zh-Hans.json +++ b/homeassistant/components/locative/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e Locative app \u7684 Webhook \u529f\u80fd\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002" }, diff --git a/homeassistant/components/locative/translations/zh-Hant.json b/homeassistant/components/locative/translations/zh-Hant.json index 8c2dcdb53ed17..b3f18defca05d 100644 --- a/homeassistant/components/locative/translations/zh-Hant.json +++ b/homeassistant/components/locative/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/lock/translations/pt-BR.json b/homeassistant/components/lock/translations/pt-BR.json index f9c4e12214cc2..02ce765e7d13a 100644 --- a/homeassistant/components/lock/translations/pt-BR.json +++ b/homeassistant/components/lock/translations/pt-BR.json @@ -4,6 +4,14 @@ "lock": "Bloquear {entity_name}", "open": "Abrir {entity_name}", "unlock": "Desbloquear {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + }, + "trigger_type": { + "locked": "{entity_name} for bloqueado", + "unlocked": "{entity_name} for desbloqueado" } }, "state": { diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 1af100397723d..28b0460ac7a18 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -71,7 +71,7 @@ EMPTY_JSON_OBJECT = "{}" UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' -HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}." +HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._" CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA @@ -598,7 +598,7 @@ def _keep_event(hass, event, entities_filter): if domain is None: return False - return entities_filter is None or entities_filter(f"{domain}.") + return entities_filter is None or entities_filter(f"{domain}._") def _augment_data_with_context( diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index b89950061694d..94c040f3b75c5 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -6,5 +6,6 @@ "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["logi_circle"] } diff --git a/homeassistant/components/logi_circle/translations/el.json b/homeassistant/components/logi_circle/translations/el.json new file mode 100644 index 0000000000000..ad26536d10b58 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/el.json @@ -0,0 +1,28 @@ +{ + "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", + "external_error": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03c1\u03bf\u03ae.", + "external_setup": "\u03a4\u03bf Logi Circle \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03c1\u03bf\u03ae.", + "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." + }, + "error": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "follow_link": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03ba\u03b1\u03b9 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c0\u03c1\u03b9\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae.", + "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": { + "auth": { + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03ba\u03b1\u03b9 **\u0391\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5** \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Logi Circle, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 **\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae** \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.\n\n[\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\u03c2]({authorization_url})", + "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Logi Circle" + }, + "user": { + "data": { + "flow_impl": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03c3\u03c9 \u03c0\u03bf\u03b9\u03bf\u03c5 \u03c0\u03b1\u03c1\u03cc\u03c7\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf Logi Circle.", + "title": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/pt-BR.json b/homeassistant/components/logi_circle/translations/pt-BR.json index a319cf0e67ee3..3086d6560f199 100644 --- a/homeassistant/components/logi_circle/translations/pt-BR.json +++ b/homeassistant/components/logi_circle/translations/pt-BR.json @@ -1,15 +1,19 @@ { "config": { "abort": { + "already_configured": "A conta j\u00e1 foi configurada", "external_error": "Exce\u00e7\u00e3o ocorreu a partir de outro fluxo.", - "external_setup": "Logi Circle configurado com sucesso a partir de outro fluxo." + "external_setup": "Logi Circle configurado com sucesso a partir de outro fluxo.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." }, "error": { - "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar." + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "auth": { - "description": "Por favor, siga o link abaixo e Aceite o acesso \u00e0 sua conta do Logi Circle, depois volte e pressione Enviar abaixo. \n\n [Link] ( {authorization_url} )", + "description": "Por favor, siga o link abaixo e **Aceite** o acesso \u00e0 sua conta do Logi Circle, depois volte e pressione **Enviar** abaixo. \n\n [Link] ( {authorization_url} )", "title": "Autenticar com o Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/sk.json b/homeassistant/components/logi_circle/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 329c9fa504d91..eed2ec45dd792 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": ["london-tube-status==0.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["london_tube_status"] } diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index cedc4fc77fe87..c15d46c5158d5 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -153,6 +153,7 @@ def _async_meteo_push_update(event: UDPEvent) -> None: ) hass.data[DOMAIN][entry.entry_id] = LookinData( + host=host, lookin_udp_subs=lookin_udp_subs, lookin_device=lookin_device, meteo_coordinator=meteo_coordinator, diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index ab6b53978befa..79cac79cb1743 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -100,7 +100,7 @@ async def async_setup_entry( class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): """An aircon or heat pump.""" - _attr_current_humidity: float | None = None # type: ignore + _attr_current_humidity: float | None = None # type: ignore[assignment] _attr_temperature_unit = TEMP_CELSIUS _attr_supported_features: int = SUPPORT_FLAGS _attr_fan_modes: list[str] = LOOKIN_FAN_MODE_IDX_TO_HASS @@ -152,8 +152,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: # an educated guess. # meteo_data: MeteoSensor = self._meteo_coordinator.data - current_temp = meteo_data.temperature - if not current_temp: + if not (current_temp := meteo_data.temperature): self._climate.hvac_mode = lookin_index.index(HVAC_MODE_AUTO) elif current_temp >= self._climate.temp_celsius: self._climate.hvac_mode = lookin_index.index(HVAC_MODE_COOL) diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 58eafd3843f9e..6ff167d86fe4c 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -18,7 +18,7 @@ LOGGER = logging.getLogger(__name__) -def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo: +def _lookin_device_to_device_info(lookin_device: Device, host: str) -> DeviceInfo: """Convert a lookin device into DeviceInfo.""" return DeviceInfo( identifiers={(DOMAIN, lookin_device.id)}, @@ -26,17 +26,19 @@ def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo: manufacturer="LOOKin", model=MODEL_NAMES[lookin_device.model], sw_version=lookin_device.firmware, + configuration_url=f"http://{host}/device", ) def _lookin_controlled_device_to_device_info( - lookin_device: Device, uuid: str, device: Climate | Remote + lookin_device: Device, uuid: str, device: Climate | Remote, host: str ) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, uuid)}, name=device.name, model=device.device_type, via_device=(DOMAIN, lookin_device.id), + configuration_url=f"http://{host}/data/{uuid}", ) @@ -62,7 +64,7 @@ def __init__(self, lookin_data: LookinData) -> None: super().__init__(lookin_data.meteo_coordinator) self._set_lookin_device_attrs(lookin_data) self._attr_device_info = _lookin_device_to_device_info( - lookin_data.lookin_device + lookin_data.lookin_device, lookin_data.host ) @@ -102,7 +104,7 @@ def __init__( self._set_lookin_device_attrs(lookin_data) self._set_lookin_entity_attrs(uuid, device, lookin_data) self._attr_device_info = _lookin_controlled_device_to_device_info( - self._lookin_device, uuid, device + self._lookin_device, uuid, device, lookin_data.host ) self._attr_unique_id = uuid self._attr_name = device.name diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index d63961b5cfa6b..7cf705403729b 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aiolookin==0.1.0"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiolookin"] } diff --git a/homeassistant/components/lookin/models.py b/homeassistant/components/lookin/models.py index 3587136c4a235..f0dffe66ec070 100644 --- a/homeassistant/components/lookin/models.py +++ b/homeassistant/components/lookin/models.py @@ -13,6 +13,7 @@ class LookinData: """Data for the lookin integration.""" + host: str lookin_udp_subs: LookinUDPSubscriptions lookin_device: Device meteo_coordinator: LookinDataUpdateCoordinator diff --git a/homeassistant/components/lookin/translations/cs.json b/homeassistant/components/lookin/translations/cs.json new file mode 100644 index 0000000000000..50dcaf1b95f79 --- /dev/null +++ b/homeassistant/components/lookin/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "device_name": { + "data": { + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/el.json b/homeassistant/components/lookin/translations/el.json new file mode 100644 index 0000000000000..1b348109b8d0e --- /dev/null +++ b/homeassistant/components/lookin/translations/el.json @@ -0,0 +1,31 @@ +{ + "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "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", + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + } + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" + }, + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/pt-BR.json b/homeassistant/components/lookin/translations/pt-BR.json new file mode 100644 index 0000000000000..38a6ae113589e --- /dev/null +++ b/homeassistant/components/lookin/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nome" + } + }, + "discovery_confirm": { + "description": "Deseja configurar {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/sk.json b/homeassistant/components/lookin/translations/sk.json new file mode 100644 index 0000000000000..561644de2dd7f --- /dev/null +++ b/homeassistant/components/lookin/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "device_name": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py new file mode 100644 index 0000000000000..02280ebd18254 --- /dev/null +++ b/homeassistant/components/lovelace/cast.py @@ -0,0 +1,204 @@ +"""Home Assistant Cast platform.""" + +from __future__ import annotations + +from pychromecast import Chromecast +from pychromecast.const import CAST_TYPE_CHROMECAST + +from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN +from homeassistant.components.cast.home_assistant_cast import ( + ATTR_URL_PATH, + ATTR_VIEW_PATH, + NO_URL_AVAILABLE_ERROR, + SERVICE_SHOW_VIEW, +) +from homeassistant.components.media_player import BrowseError, BrowseMedia +from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.network import NoURLAvailableError, get_url + +from .const import DOMAIN, ConfigNotFound +from .dashboard import LovelaceConfig + +DEFAULT_DASHBOARD = "_default_" + + +async def async_get_media_browser_root_object( + hass: HomeAssistant, cast_type: str +) -> list[BrowseMedia]: + """Create a root object for media browsing.""" + if cast_type != CAST_TYPE_CHROMECAST: + return [] + return [ + BrowseMedia( + title="Lovelace", + media_class=MEDIA_CLASS_APP, + media_content_id="", + media_content_type=DOMAIN, + thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", + can_play=False, + can_expand=True, + ) + ] + + +async def async_browse_media( + hass: HomeAssistant, + media_content_type: str, + media_content_id: str, + cast_type: str, +) -> BrowseMedia | None: + """Browse media.""" + if media_content_type != DOMAIN: + return None + + try: + get_url(hass, require_ssl=True, prefer_external=True) + except NoURLAvailableError as err: + raise BrowseError(NO_URL_AVAILABLE_ERROR) from err + + # List dashboards. + if not media_content_id: + children = [ + BrowseMedia( + title="Default", + media_class=MEDIA_CLASS_APP, + media_content_id=DEFAULT_DASHBOARD, + media_content_type=DOMAIN, + thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", + can_play=True, + can_expand=False, + ) + ] + for url_path in hass.data[DOMAIN]["dashboards"]: + if url_path is None: + continue + + info = await _get_dashboard_info(hass, url_path) + children.append(_item_from_info(info)) + + root = (await async_get_media_browser_root_object(hass, CAST_TYPE_CHROMECAST))[ + 0 + ] + root.children = children + return root + + try: + info = await _get_dashboard_info(hass, media_content_id) + except ValueError as err: + raise BrowseError(f"Dashboard {media_content_id} not found") from err + + children = [] + + for view in info["views"]: + children.append( + BrowseMedia( + title=view["title"], + media_class=MEDIA_CLASS_APP, + media_content_id=f'{info["url_path"]}/{view["path"]}', + media_content_type=DOMAIN, + thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", + can_play=True, + can_expand=False, + ) + ) + + root = _item_from_info(info) + root.children = children + return root + + +async def async_play_media( + hass: HomeAssistant, + cast_entity_id: str, + chromecast: Chromecast, + media_type: str, + media_id: str, +) -> bool: + """Play media.""" + if media_type != DOMAIN: + return False + + if "/" in media_id: + url_path, view_path = media_id.split("/", 1) + else: + url_path = media_id + try: + info = await _get_dashboard_info(hass, media_id) + except ValueError as err: + raise HomeAssistantError(f"Invalid dashboard {media_id} specified") from err + view_path = info["views"][0]["path"] if info["views"] else "0" + + data = { + ATTR_ENTITY_ID: cast_entity_id, + ATTR_VIEW_PATH: view_path, + } + if url_path != DEFAULT_DASHBOARD: + data[ATTR_URL_PATH] = url_path + + await hass.services.async_call( + CAST_DOMAIN, + SERVICE_SHOW_VIEW, + data, + blocking=True, + ) + return True + + +async def _get_dashboard_info(hass, url_path): + """Load a dashboard and return info on views.""" + if url_path == DEFAULT_DASHBOARD: + url_path = None + dashboard: LovelaceConfig | None = hass.data[DOMAIN]["dashboards"].get(url_path) + + if dashboard is None: + raise ValueError("Invalid dashboard specified") + + try: + config = await dashboard.async_load(False) + except ConfigNotFound: + config = None + + if dashboard.url_path is None: + url_path = DEFAULT_DASHBOARD + title = "Default" + else: + url_path = dashboard.url_path + title = config.get("title", url_path) if config else url_path + + views = [] + data = { + "title": title, + "url_path": url_path, + "views": views, + } + + if config is None: + return data + + for idx, view in enumerate(config["views"]): + path = view.get("path", f"{idx}") + views.append( + { + "title": view.get("title", path), + "path": path, + } + ) + + return data + + +@callback +def _item_from_info(info: dict) -> BrowseMedia: + """Convert dashboard info to browse item.""" + return BrowseMedia( + title=info["title"], + media_class=MEDIA_CLASS_APP, + media_content_id=info["url_path"], + media_content_type=DOMAIN, + thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", + can_play=True, + can_expand=len(info["views"]) > 1, + ) diff --git a/homeassistant/components/lovelace/translations/bg.json b/homeassistant/components/lovelace/translations/bg.json index 3a9370b548620..4a755519aefb0 100644 --- a/homeassistant/components/lovelace/translations/bg.json +++ b/homeassistant/components/lovelace/translations/bg.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "dashboards": "\u0422\u0430\u0431\u043b\u0430", "mode": "\u0420\u0435\u0436\u0438\u043c", "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", "views": "\u0418\u0437\u0433\u043b\u0435\u0434\u0438" diff --git a/homeassistant/components/lovelace/translations/pt-BR.json b/homeassistant/components/lovelace/translations/pt-BR.json new file mode 100644 index 0000000000000..2ff25d17161dd --- /dev/null +++ b/homeassistant/components/lovelace/translations/pt-BR.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Pain\u00e9is", + "mode": "Modo", + "resources": "Recursos", + "views": "Visualiza\u00e7\u00f5es" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 705bb7ecb4b9a..2d61852689a8c 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.11"], "codeowners": ["@mzdrale"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["openwrt_luci_rpc"] } diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index ec3da32a76db8..255dc8c52eabe 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -6,5 +6,6 @@ "requirements": ["luftdaten==0.7.2"], "codeowners": ["@fabaff", "@frenck"], "quality_scale": "gold", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["luftdaten"] } diff --git a/homeassistant/components/luftdaten/translations/el.json b/homeassistant/components/luftdaten/translations/el.json new file mode 100644 index 0000000000000..59519c251ef2a --- /dev/null +++ b/homeassistant/components/luftdaten/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_sensor": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2 \u03ae \u03ac\u03ba\u03c5\u03c1\u03bf\u03c2" + }, + "step": { + "user": { + "data": { + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7", + "station_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 Luftdaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/hu.json b/homeassistant/components/luftdaten/translations/hu.json index 2fa90c23ca8d2..f7a241533da02 100644 --- a/homeassistant/components/luftdaten/translations/hu.json +++ b/homeassistant/components/luftdaten/translations/hu.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "show_on_map": "Mutasd a t\u00e9rk\u00e9pen", + "show_on_map": "Megjelen\u00edt\u00e9s a t\u00e9rk\u00e9pen", "station_id": "Luftdaten \u00e9rz\u00e9kel\u0151 ID" }, "title": "Luftdaten be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/luftdaten/translations/id.json b/homeassistant/components/luftdaten/translations/id.json index 96ec6d5f20ff1..11fc29170610a 100644 --- a/homeassistant/components/luftdaten/translations/id.json +++ b/homeassistant/components/luftdaten/translations/id.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Tampilkan di peta", - "station_id": "ID Sensor Luftdaten" + "station_id": "ID Sensor" }, "title": "Konfigurasikan Luftdaten" } diff --git a/homeassistant/components/luftdaten/translations/pl.json b/homeassistant/components/luftdaten/translations/pl.json index 60fc07142286d..7045a0a3dbeb1 100644 --- a/homeassistant/components/luftdaten/translations/pl.json +++ b/homeassistant/components/luftdaten/translations/pl.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Poka\u017c na mapie", - "station_id": "ID sensora Luftdaten" + "station_id": "ID sensora" }, "title": "Konfiguracja Luftdaten" } diff --git a/homeassistant/components/luftdaten/translations/pt-BR.json b/homeassistant/components/luftdaten/translations/pt-BR.json index 3884170c2e014..b4cdaf000ab2d 100644 --- a/homeassistant/components/luftdaten/translations/pt-BR.json +++ b/homeassistant/components/luftdaten/translations/pt-BR.json @@ -1,12 +1,14 @@ { "config": { "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", "invalid_sensor": "Sensor n\u00e3o dispon\u00edvel ou inv\u00e1lido" }, "step": { "user": { "data": { - "show_on_map": "Mostrar no mapa", + "show_on_map": "Mostrar no mapa?", "station_id": "ID do Sensor Luftdaten" }, "title": "Definir Luftdaten" diff --git a/homeassistant/components/luftdaten/translations/ru.json b/homeassistant/components/luftdaten/translations/ru.json index 5891bb1e3ddba..82813ee3a5337 100644 --- a/homeassistant/components/luftdaten/translations/ru.json +++ b/homeassistant/components/luftdaten/translations/ru.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", "station_id": "ID \u0434\u0430\u0442\u0447\u0438\u043a\u0430" }, "title": "Luftdaten" diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 126fa407a3792..53ab1e6af4702 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": ["lupupy==0.0.24"], "codeowners": ["@majuss"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["lupupy"] } diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 83c4ee7234573..7c3e66c71275c 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.8"], "codeowners": ["@JonGilmore"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pylutron"] } diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 0408f547f2546..ebd9e04133221 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -221,9 +221,7 @@ def _async_subscribe_pico_remote_events( @callback def _async_button_event(button_id, event_type): - device = button_devices_by_id.get(button_id) - - if not device: + if not (device := button_devices_by_id.get(button_id)): return if event_type == BUTTON_STATUS_PRESSED: diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index b198d5ddbee6e..74819e25e8edc 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Lutron Caseta.""" +from __future__ import annotations + import asyncio import logging import os @@ -17,6 +19,7 @@ from .const import ( ABORT_REASON_CANNOT_CONNECT, + BRIDGE_DEVICE_ID, BRIDGE_TIMEOUT, CONF_CA_CERTS, CONF_CERTFILE, @@ -101,7 +104,7 @@ async def async_step_link(self, user_input=None): if ( not self.attempted_tls_validation and await self.hass.async_add_executor_job(self._tls_assets_exist) - and await self.async_validate_connectable_bridge_config() + and await self.async_get_lutron_id() ): self.tls_assets_validated = True self.attempted_tls_validation = True @@ -177,7 +180,7 @@ async def async_step_import(self, import_info): self.data[CONF_CERTFILE] = import_info[CONF_CERTFILE] self.data[CONF_CA_CERTS] = import_info[CONF_CA_CERTS] - if not await self.async_validate_connectable_bridge_config(): + if not (lutron_id := await self.async_get_lutron_id()): # Ultimately we won't have a dedicated step for import failure, but # in order to keep configuration.yaml-based configs transparently # working without requiring further actions from the user, we don't @@ -189,6 +192,8 @@ async def async_step_import(self, import_info): # will require users to go through a confirmation flow for imports). return await self.async_step_import_failed() + await self.async_set_unique_id(lutron_id, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry(title=ENTRY_DEFAULT_TITLE, data=self.data) async def async_step_import_failed(self, user_input=None): @@ -204,10 +209,8 @@ async def async_step_import_failed(self, user_input=None): return self.async_abort(reason=ABORT_REASON_CANNOT_CONNECT) - async def async_validate_connectable_bridge_config(self): + async def async_get_lutron_id(self) -> str | None: """Check if we can connect to the bridge with the current config.""" - bridge = None - try: bridge = Smartbridge.create_tls( hostname=self.data[CONF_HOST], @@ -220,18 +223,23 @@ async def async_validate_connectable_bridge_config(self): "Invalid certificate used to connect to bridge at %s", self.data[CONF_HOST], ) - return False + return None - connected_ok = False try: async with async_timeout.timeout(BRIDGE_TIMEOUT): await bridge.connect() - connected_ok = bridge.is_connected() except asyncio.TimeoutError: _LOGGER.error( "Timeout while trying to connect to bridge at %s", self.data[CONF_HOST], ) - - await bridge.close() - return connected_ok + else: + if not bridge.is_connected(): + return None + devices = bridge.get_devices() + bridge_device = devices[BRIDGE_DEVICE_ID] + return hex(bridge_device["serial"])[2:].zfill(8) + finally: + await bridge.close() + + return None diff --git a/homeassistant/components/lutron_caseta/diagnostics.py b/homeassistant/components/lutron_caseta/diagnostics.py new file mode 100644 index 0000000000000..7ae0b5c40a9f6 --- /dev/null +++ b/homeassistant/components/lutron_caseta/diagnostics.py @@ -0,0 +1,31 @@ +"""Diagnostics support for lutron_caseta.""" +from __future__ import annotations + +from typing import Any + +from pylutron_caseta.smartbridge import Smartbridge + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import BRIDGE_LEAP, DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + bridge: Smartbridge = hass.data[DOMAIN][entry.entry_id][BRIDGE_LEAP] + return { + "entry": { + "title": entry.title, + "data": dict(entry.data), + }, + "data": { + "devices": bridge.devices, + "buttons": bridge.buttons, + "scenes": bridge.scenes, + "occupancy_groups": bridge.occupancy_groups, + "areas": bridge.areas, + }, + } diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index fc29aa8ced7fe..206d8b51233c5 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -9,5 +9,6 @@ "models": ["Smart Bridge"] }, "codeowners": ["@swails", "@bdraco"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pylutron_caseta"] } diff --git a/homeassistant/components/lutron_caseta/translations/el.json b/homeassistant/components/lutron_caseta/translations/el.json index 4285f5fcbd0cb..f0c1ec35450b2 100644 --- a/homeassistant/components/lutron_caseta/translations/el.json +++ b/homeassistant/components/lutron_caseta/translations/el.json @@ -1,8 +1,13 @@ { "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", "not_lutron_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 Lutron" }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "flow_title": "{name} ({host})", "step": { "import_failed": { @@ -14,12 +19,18 @@ "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\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" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1" } } }, "device_automation": { "trigger_subtype": { + "button_1": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "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", "close_1": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf 1", @@ -43,7 +54,23 @@ "open_2": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 2", "open_3": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 3", "open_4": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 4", - "open_all": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03cc\u03bb\u03c9\u03bd" + "open_all": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03cc\u03bb\u03c9\u03bd", + "raise": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7", + "raise_1": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 1", + "raise_2": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 2", + "raise_3": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 3", + "raise_4": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 4", + "raise_all": "\u03a3\u03b7\u03ba\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03cc\u03bb\u03b1", + "stop": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae (\u03b1\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03bf)", + "stop_1": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae 1", + "stop_2": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae 2", + "stop_3": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae 3", + "stop_4": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae 4", + "stop_all": "\u03a3\u03c4\u03b1\u03bc\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03cc\u03bb\u03b1" + }, + "trigger_type": { + "press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", + "release": "\u0391\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pt-BR.json b/homeassistant/components/lutron_caseta/translations/pt-BR.json index 091f7990989fb..28a85a8820dea 100644 --- a/homeassistant/components/lutron_caseta/translations/pt-BR.json +++ b/homeassistant/components/lutron_caseta/translations/pt-BR.json @@ -1,17 +1,76 @@ { "config": { "abort": { - "already_configured": "Ponte Cas\u00e9ta j\u00e1 configurada.", - "cannot_connect": "Instala\u00e7\u00e3o cancelada da ponte Cas\u00e9ta devido \u00e0 falha na conex\u00e3o." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "not_lutron_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Lutron" }, "error": { - "cannot_connect": "Falha ao conectar \u00e0 ponte Cas\u00e9ta; verifique sua configura\u00e7\u00e3o de endere\u00e7o e certificado." + "cannot_connect": "Falha ao conectar" }, + "flow_title": "{name} ( {host} )", "step": { "import_failed": { "description": "N\u00e3o foi poss\u00edvel configurar a ponte (host: {host}) importada do configuration.yaml.", "title": "Falha ao importar a configura\u00e7\u00e3o da ponte Cas\u00e9ta." + }, + "link": { + "description": "Para parear com {name} ( {host} ), ap\u00f3s enviar este formul\u00e1rio, pressione o bot\u00e3o preto na parte de tr\u00e1s da ponte.", + "title": "Parear com a ponte" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Digite o endere\u00e7o IP do dispositivo.", + "title": "Conecte-se automaticamente \u00e0 ponte" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primeiro bot\u00e3o", + "button_2": "Segundo bot\u00e3o", + "button_3": "Terceiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o", + "close_1": "Fechar 1", + "close_2": "Fechar 2", + "close_3": "Fechar 3", + "close_4": "Fechar 4", + "close_all": "Feche tudo", + "group_1_button_1": "Primeiro bot\u00e3o do primeiro grupo", + "group_1_button_2": "Primeiro bot\u00e3o segundo grupo", + "group_2_button_1": "Primeiro bot\u00e3o do segundo grupo", + "group_2_button_2": "Segundo bot\u00e3o do segundo grupo", + "lower": "Abaixar", + "lower_1": "Inferior 1", + "lower_2": "Inferior 2", + "lower_3": "Inferior 3", + "lower_4": "Inferior 4", + "lower_all": "Baixar tudo", + "off": "Desligado", + "on": "Ligado", + "open_1": "Abrir 1", + "open_2": "Abrir 2", + "open_3": "Abrir 3", + "open_4": "Abrir 4", + "open_all": "Abra tudo", + "raise": "Aumentar", + "raise_1": "Aumentar 1", + "raise_2": "Aumentar 2", + "raise_3": "Aumentar 3", + "raise_4": "Aumentar 4", + "raise_all": "Aumentar tudo", + "stop": "Parar (favorito)", + "stop_1": "Parar 1", + "stop_2": "Parar 2", + "stop_3": "Parar 3", + "stop_4": "Parar 4", + "stop_all": "Parar tudo" + }, + "trigger_type": { + "press": "\"{subtype}\" pressionado", + "release": "\"{subtype}\" lan\u00e7ado" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index c45d7fb38e98f..146a33972971e 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -21,5 +21,6 @@ "macaddress": "00D02D*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aiolyric"] } diff --git a/homeassistant/components/lyric/translations/el.json b/homeassistant/components/lyric/translations/el.json index f238d2952cbfe..e932195087407 100644 --- a/homeassistant/components/lyric/translations/el.json +++ b/homeassistant/components/lyric/translations/el.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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.", + "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" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Lyric \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2." + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Lyric \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \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" } } } diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json new file mode 100644 index 0000000000000..907a396d5e2e1 --- /dev/null +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Lyric precisa autenticar novamente sua conta.", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/sk.json b/homeassistant/components/lyric/translations/sk.json new file mode 100644 index 0000000000000..520a3afd6d921 --- /dev/null +++ b/homeassistant/components/lyric/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 84a2addc3e144..57b31e03dc7b4 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": ["magicseaweed==1.0.3"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["magicseaweed"] } diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index d8d5182816b8d..2d16786bd3901 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pymailgunner==1.4"], "dependencies": ["webhook"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pymailgunner"] } diff --git a/homeassistant/components/mailgun/translations/bg.json b/homeassistant/components/mailgun/translations/bg.json index d9bcdd38dfd85..0a3782dc2c3da 100644 --- a/homeassistant/components/mailgun/translations/bg.json +++ b/homeassistant/components/mailgun/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/mailgun/translations/ca.json b/homeassistant/components/mailgun/translations/ca.json index 6584b15b3ad49..5959fce55c2be 100644 --- a/homeassistant/components/mailgun/translations/ca.json +++ b/homeassistant/components/mailgun/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/mailgun/translations/de.json b/homeassistant/components/mailgun/translations/de.json index 118192b65160f..34c251770cc33 100644 --- a/homeassistant/components/mailgun/translations/de.json +++ b/homeassistant/components/mailgun/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/mailgun/translations/el.json b/homeassistant/components/mailgun/translations/el.json index aecb2ee553fa4..bc2821c088199 100644 --- a/homeassistant/components/mailgun/translations/el.json +++ b/homeassistant/components/mailgun/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf [Webhooks with Mailgun]({mailgun_url}). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: `{webhook_url}`\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/json \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Mailgun;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Webhook Mailgun" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/en.json b/homeassistant/components/mailgun/translations/en.json index ec6732304bd86..928ab40e1af7e 100644 --- a/homeassistant/components/mailgun/translations/en.json +++ b/homeassistant/components/mailgun/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/mailgun/translations/et.json b/homeassistant/components/mailgun/translations/et.json index 4f3469d13577e..7e659cfe8ac72 100644 --- a/homeassistant/components/mailgun/translations/et.json +++ b/homeassistant/components/mailgun/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json index b822522af10ea..a8d4b08ed83d9 100644 --- a/homeassistant/components/mailgun/translations/fr.json +++ b/homeassistant/components/mailgun/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/mailgun/translations/he.json b/homeassistant/components/mailgun/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/mailgun/translations/he.json +++ b/homeassistant/components/mailgun/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index b40c4316bba22..17b45578ba3de 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/mailgun/translations/id.json b/homeassistant/components/mailgun/translations/id.json index b58deb171bef0..e428cc3721381 100644 --- a/homeassistant/components/mailgun/translations/id.json +++ b/homeassistant/components/mailgun/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/mailgun/translations/it.json b/homeassistant/components/mailgun/translations/it.json index fdefe8992e883..0131b39c22b06 100644 --- a/homeassistant/components/mailgun/translations/it.json +++ b/homeassistant/components/mailgun/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index cacb7e9250298..58818dd99de6a 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/mailgun/translations/nb.json b/homeassistant/components/mailgun/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/mailgun/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/nl.json b/homeassistant/components/mailgun/translations/nl.json index dea33946af51f..5e84c62a314c3 100644 --- a/homeassistant/components/mailgun/translations/nl.json +++ b/homeassistant/components/mailgun/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/mailgun/translations/no.json b/homeassistant/components/mailgun/translations/no.json index 4cc080805ac57..77576133365d3 100644 --- a/homeassistant/components/mailgun/translations/no.json +++ b/homeassistant/components/mailgun/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/mailgun/translations/pl.json b/homeassistant/components/mailgun/translations/pl.json index 931cd7a816702..dcd95ab75caac 100644 --- a/homeassistant/components/mailgun/translations/pl.json +++ b/homeassistant/components/mailgun/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/mailgun/translations/pt-BR.json b/homeassistant/components/mailgun/translations/pt-BR.json index 36e14f97645c1..7985d9ddbfb8f 100644 --- a/homeassistant/components/mailgun/translations/pt-BR.json +++ b/homeassistant/components/mailgun/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks com Mailgun]({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/mailgun/translations/ru.json b/homeassistant/components/mailgun/translations/ru.json index 2e79776f89a08..00aa509c95822 100644 --- a/homeassistant/components/mailgun/translations/ru.json +++ b/homeassistant/components/mailgun/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json index 3918614af2e93..6f7efc7d8b398 100644 --- a/homeassistant/components/mailgun/translations/tr.json +++ b/homeassistant/components/mailgun/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/mailgun/translations/uk.json b/homeassistant/components/mailgun/translations/uk.json index d999b52085a21..0c31d070bec29 100644 --- a/homeassistant/components/mailgun/translations/uk.json +++ b/homeassistant/components/mailgun/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/mailgun/translations/zh-Hans.json b/homeassistant/components/mailgun/translations/zh-Hans.json index 97a827f3d6b90..35de7e89ed193 100644 --- a/homeassistant/components/mailgun/translations/zh-Hans.json +++ b/homeassistant/components/mailgun/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Mailgun \u7684 Webhook]({mailgun_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" }, diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json index 508a652ce9b5a..67f0a6dc98756 100644 --- a/homeassistant/components/mailgun/translations/zh-Hant.json +++ b/homeassistant/components/mailgun/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 832631878ebeb..fb72ef8b1db66 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -1,6 +1,6 @@ { "domain": "manual", - "name": "Manual", + "name": "Manual Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/manual", "codeowners": [], "quality_scale": "internal", diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 56b13ce90a7fc..6e0cc30e207c6 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -1,6 +1,6 @@ { "domain": "manual_mqtt", - "name": "Manual MQTT", + "name": "Manual MQTT Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "dependencies": ["mqtt"], "codeowners": [], diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index f53e0deecd70b..c07f9b2a270a4 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": ["speak2mary==1.4.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["speak2mary"] } diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index cd393002e1de3..e4e8ceb53ee0d 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": ["Mastodon.py==1.5.1"], "codeowners": ["@fabaff"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["mastodon"] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 4e31b99c172b7..e3d7b275de7c2 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": ["matrix-client==0.4.0"], "codeowners": ["@tinloaf"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["matrix_client"] } diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index fa4bcc44cc620..7b9b402cb8dd6 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": ["maxcube-api==0.4.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["maxcube"] } diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index e00049101f908..a75c7f99e4c50 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pymazda==0.3.2"], "codeowners": ["@bdr99"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pymazda"] } diff --git a/homeassistant/components/mazda/translations/el.json b/homeassistant/components/mazda/translations/el.json index 8fd08509174f4..c95e8ad474779 100644 --- a/homeassistant/components/mazda/translations/el.json +++ b/homeassistant/components/mazda/translations/el.json @@ -1,8 +1,20 @@ { "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", + "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": { + "account_locked": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf\u03c2. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", + "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": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "user": { "data": { + "email": "Email", + "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" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae MyMazda \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac.", diff --git a/homeassistant/components/mazda/translations/pt-BR.json b/homeassistant/components/mazda/translations/pt-BR.json new file mode 100644 index 0000000000000..7b28450bfd093 --- /dev/null +++ b/homeassistant/components/mazda/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "account_locked": "Conta bloqueada. Por favor, tente novamente mais tarde.", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha", + "region": "Regi\u00e3o" + }, + "description": "Digite o endere\u00e7o de e-mail e senha que voc\u00ea usa para entrar no aplicativo MyMazda.", + "title": "Mazda Connected Services - Adicionar conta" + } + } + }, + "title": "Servi\u00e7os conectados Mazda" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/sk.json b/homeassistant/components/mazda/translations/sk.json new file mode 100644 index 0000000000000..f8b6dfeea813e --- /dev/null +++ b/homeassistant/components/mazda/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 2fad5acc0ce69..e6f04ad1171c2 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -7,5 +7,6 @@ "adafruit-circuitpython-mcp230xx==2.2.2" ], "codeowners": ["@jardiamj"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["adafruit_mcp230xx"] } diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 6444aa17d7dcb..65efae00277a7 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "loggers": ["youtube_dl"] } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 587c75dd035d7..99d493a75c4fc 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -59,7 +59,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - datetime, ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent @@ -67,7 +66,8 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .const import ( +from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F401 +from .const import ( # noqa: F401 ATTR_APP_ID, ATTR_APP_NAME, ATTR_GROUP_MEMBERS, @@ -97,6 +97,7 @@ ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, + CONTENT_AUTH_EXPIRY_TIME, DOMAIN, MEDIA_CLASS_DIRECTORY, REPEAT_MODES, @@ -998,25 +999,7 @@ async def _async_fetch_image_from_cache( async def _async_fetch_image(self, url: str) -> tuple[bytes | None, str | None]: """Retrieve an image.""" - content, content_type = (None, None) - websession = async_get_clientsession(self.hass) - with suppress(asyncio.TimeoutError), async_timeout.timeout(10): - response = await websession.get(url) - if response.status == HTTPStatus.OK: - content = await response.read() - if content_type := response.headers.get(CONTENT_TYPE): - content_type = content_type.split(";")[0] - - if content is None: - url_parts = URL(url) - if url_parts.user is not None: - url_parts = url_parts.with_user("xxxx") - if url_parts.password is not None: - url_parts = url_parts.with_password("xxxxxxxx") - url = str(url_parts) - _LOGGER.warning("Error retrieving proxied image from %s", url) - - return content, content_type + return await async_fetch_image(_LOGGER, self.hass, url) def get_browse_image_url( self, @@ -1206,72 +1189,26 @@ async def websocket_browse_media(hass, connection, msg): connection.send_result(msg["id"], payload) -class BrowseMedia: - """Represent a browsable media file.""" - - def __init__( - self, - *, - media_class: str, - media_content_id: str, - media_content_type: str, - title: str, - can_play: bool, - can_expand: bool, - children: list[BrowseMedia] | None = None, - children_media_class: str | None = None, - thumbnail: str | None = None, - ) -> None: - """Initialize browse media item.""" - self.media_class = media_class - self.media_content_id = media_content_id - self.media_content_type = media_content_type - self.title = title - self.can_play = can_play - self.can_expand = can_expand - self.children = children - self.children_media_class = children_media_class - self.thumbnail = thumbnail - - def as_dict(self, *, parent: bool = True) -> dict: - """Convert Media class to browse media dictionary.""" - if self.children_media_class is None: - self.calculate_children_class() - - response = { - "title": self.title, - "media_class": self.media_class, - "media_content_type": self.media_content_type, - "media_content_id": self.media_content_id, - "can_play": self.can_play, - "can_expand": self.can_expand, - "children_media_class": self.children_media_class, - "thumbnail": self.thumbnail, - } - - if not parent: - return response - - if self.children: - response["children"] = [ - child.as_dict(parent=False) for child in self.children - ] - else: - response["children"] = [] - - return response - - def calculate_children_class(self) -> None: - """Count the children media classes and calculate the correct class.""" - if self.children is None or len(self.children) == 0: - return - - self.children_media_class = MEDIA_CLASS_DIRECTORY - - proposed_class = self.children[0].media_class - if all(child.media_class == proposed_class for child in self.children): - self.children_media_class = proposed_class - - def __repr__(self): - """Return representation of browse media.""" - return f"" +async def async_fetch_image( + logger: logging.Logger, hass: HomeAssistant, url: str +) -> tuple[bytes | None, str | None]: + """Retrieve an image.""" + content, content_type = (None, None) + websession = async_get_clientsession(hass) + with suppress(asyncio.TimeoutError), async_timeout.timeout(10): + response = await websession.get(url) + if response.status == HTTPStatus.OK: + content = await response.read() + if content_type := response.headers.get(CONTENT_TYPE): + content_type = content_type.split(";")[0] + + if content is None: + url_parts = URL(url) + if url_parts.user is not None: + url_parts = url_parts.with_user("xxxx") + if url_parts.password is not None: + url_parts = url_parts.with_password("xxxxxxxx") + url = str(url_parts) + logger.warning("Error retrieving proxied image from %s", url) + + return content, content_type diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py new file mode 100644 index 0000000000000..fa82504281713 --- /dev/null +++ b/homeassistant/components/media_player/browse_media.py @@ -0,0 +1,116 @@ +"""Browse media features for media player.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any +from urllib.parse import quote + +import yarl + +from homeassistant.components.http.auth import async_sign_path +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.network import get_url, is_hass_url + +from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY + + +@callback +def async_process_play_media_url( + hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False +) -> str: + """Update a media URL with authentication if it points at Home Assistant.""" + if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): + return media_content_id + + parsed = yarl.URL(media_content_id) + + if parsed.query: + logging.getLogger(__name__).debug( + "Not signing path for content with query param" + ) + else: + signed_path = async_sign_path( + hass, + quote(parsed.path), + timedelta(seconds=CONTENT_AUTH_EXPIRY_TIME), + ) + media_content_id = str(parsed.join(yarl.URL(signed_path))) + + # convert relative URL to absolute URL + if media_content_id[0] == "/" and not allow_relative_url: + media_content_id = f"{get_url(hass)}{media_content_id}" + + return media_content_id + + +class BrowseMedia: + """Represent a browsable media file.""" + + def __init__( + self, + *, + media_class: str, + media_content_id: str, + media_content_type: str, + title: str, + can_play: bool, + can_expand: bool, + children: list[BrowseMedia] | None = None, + children_media_class: str | None = None, + thumbnail: str | None = None, + not_shown: int = 0, + ) -> None: + """Initialize browse media item.""" + self.media_class = media_class + self.media_content_id = media_content_id + self.media_content_type = media_content_type + self.title = title + self.can_play = can_play + self.can_expand = can_expand + self.children = children + self.children_media_class = children_media_class + self.thumbnail = thumbnail + self.not_shown = not_shown + + def as_dict(self, *, parent: bool = True) -> dict: + """Convert Media class to browse media dictionary.""" + if self.children_media_class is None and self.children: + self.calculate_children_class() + + response: dict[str, Any] = { + "title": self.title, + "media_class": self.media_class, + "media_content_type": self.media_content_type, + "media_content_id": self.media_content_id, + "children_media_class": self.children_media_class, + "can_play": self.can_play, + "can_expand": self.can_expand, + "thumbnail": self.thumbnail, + } + + if not parent: + return response + + response["not_shown"] = self.not_shown + + if self.children: + response["children"] = [ + child.as_dict(parent=False) for child in self.children + ] + else: + response["children"] = [] + + return response + + def calculate_children_class(self) -> None: + """Count the children media classes and calculate the correct class.""" + self.children_media_class = MEDIA_CLASS_DIRECTORY + assert self.children is not None + proposed_class = self.children[0].media_class + if all(child.media_class == proposed_class for child in self.children): + self.children_media_class = proposed_class + + def __repr__(self) -> str: + """Return representation of browse media.""" + return f"" diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 67f4331aa60c4..e7b16f6ac8814 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,4 +1,6 @@ """Provides the constants needed for component.""" +# How long our auth signature on the content should be valid for +CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 ATTR_APP_ID = "app_id" ATTR_APP_NAME = "app_name" diff --git a/homeassistant/components/media_player/translations/el.json b/homeassistant/components/media_player/translations/el.json index 73b20532035a4..e3a300af77dd2 100644 --- a/homeassistant/components/media_player/translations/el.json +++ b/homeassistant/components/media_player/translations/el.json @@ -1,5 +1,12 @@ { "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", + "is_off": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "is_on": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "is_paused": "{entity_name} \u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "is_playing": "{entity_name} \u03c0\u03b1\u03af\u03b6\u03b5\u03b9" + }, "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2", "idle": "{entity_name} \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index f1ffc44957eef..0dfc063a0356a 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} est\u00e1 reproduciendo" }, "trigger_type": { + "changed_states": "{entity_name} ha cambiado de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", "playing": "{entity_name} comienza a reproducirse", diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index 9ecdd19037f8c..bcda6d770a3f8 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} joue" }, "trigger_type": { + "changed_states": "{entity_name} a chang\u00e9 d'\u00e9tat", "idle": "{entity_name} devient inactif", "paused": "{entity_name} est mis en pause", "playing": "{entity_name} commence \u00e0 jouer", diff --git a/homeassistant/components/media_player/translations/id.json b/homeassistant/components/media_player/translations/id.json index e759f88a15a36..9446d1e9e89a3 100644 --- a/homeassistant/components/media_player/translations/id.json +++ b/homeassistant/components/media_player/translations/id.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} sedang memutar" }, "trigger_type": { + "changed_states": "{entity_name} mengubah status", "idle": "{entity_name} menjadi siaga", "paused": "{entity_name} dijeda", "playing": "{entity_name} mulai memutar", diff --git a/homeassistant/components/media_player/translations/nb.json b/homeassistant/components/media_player/translations/nb.json index d533b6e447123..f0621dde7bebc 100644 --- a/homeassistant/components/media_player/translations/nb.json +++ b/homeassistant/components/media_player/translations/nb.json @@ -6,7 +6,7 @@ "on": "P\u00e5", "paused": "Pauset", "playing": "Spiller", - "standby": "Avventer" + "standby": "Hvilemodus" } }, "title": "Mediaspiller" diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json index 6ad22742533bf..5fae215f4f9c8 100644 --- a/homeassistant/components/media_player/translations/nl.json +++ b/homeassistant/components/media_player/translations/nl.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} wordt afgespeeld" }, "trigger_type": { + "changed_states": "{entity_name} veranderde van status", "idle": "{entity_name} wordt inactief", "paused": "{entity_name} is gepauzeerd", "playing": "{entity_name} begint te spelen", diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json index 2a70661d78883..08c664a909d00 100644 --- a/homeassistant/components/media_player/translations/pl.json +++ b/homeassistant/components/media_player/translations/pl.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} odtwarza media" }, "trigger_type": { + "changed_states": "{entity_name} zmieni\u0142o stan", "idle": "odtwarzacz {entity_name} stanie si\u0119 bezczynny", "paused": "odtwarzacz {entity_name} zostanie wstrzymany", "playing": "odtwarzacz {entity_name} rozpocznie odtwarzanie", diff --git a/homeassistant/components/media_player/translations/pt-BR.json b/homeassistant/components/media_player/translations/pt-BR.json index f980d5d200431..147f66ec0e72d 100644 --- a/homeassistant/components/media_player/translations/pt-BR.json +++ b/homeassistant/components/media_player/translations/pt-BR.json @@ -1,13 +1,30 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e1 ocioso", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_paused": "{entity_name} est\u00e1 pausado", + "is_playing": "{entity_name} est\u00e1 reproduzindo" + }, + "trigger_type": { + "changed_states": "{entity_name} ligado ou desligado", + "idle": "{entity_name} ficar ocioso", + "paused": "{entity_name} for pausado", + "playing": "{entity_name} come\u00e7ar a reproduzir", + "turned_off": "{entity_name} for desligado", + "turned_on": "{entity_name} for ligado" + } + }, "state": { "_": { "idle": "Ocioso", "off": "Desligado", "on": "Ligado", "paused": "Pausado", - "playing": "Tocando", + "playing": "Reproduzindo", "standby": "Em espera" } }, - "title": "Media player" + "title": "Navegador multim\u00eddia" } \ No newline at end of file diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 2374eca2e6a80..3c42016f8f7c0 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,19 +2,20 @@ from __future__ import annotations from collections.abc import Callable -from datetime import timedelta from typing import Any -from urllib.parse import quote import voluptuous as vol from homeassistant.components import frontend, websocket_api -from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, + CONTENT_AUTH_EXPIRY_TIME, BrowseError, BrowseMedia, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( @@ -28,8 +29,6 @@ from .error import MediaSourceError, Unresolvable from .models import BrowseMediaSource, MediaSourceItem, PlayMedia -DEFAULT_EXPIRY_TIME = 3600 * 24 - __all__ = [ "DOMAIN", "is_media_source_id", @@ -85,11 +84,16 @@ def _get_media_item( ) -> MediaSourceItem: """Return media item.""" if media_content_id: - return MediaSourceItem.from_uri(hass, media_content_id) + item = MediaSourceItem.from_uri(hass, media_content_id) + else: + # We default to our own domain if its only one registered + domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN + return MediaSourceItem(hass, domain, "") - # We default to our own domain if its only one registered - domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN - return MediaSourceItem(hass, domain, "") + if item.domain is not None and item.domain not in hass.data[DOMAIN]: + raise ValueError("Unknown media source") + + return item @bind_hass @@ -103,14 +107,19 @@ async def async_browse_media( if DOMAIN not in hass.data: raise BrowseError("Media Source not loaded") - item = await _get_media_item(hass, media_content_id).async_browse() + try: + item = await _get_media_item(hass, media_content_id).async_browse() + except ValueError as err: + raise BrowseError(str(err)) from err if content_filter is None or item.children is None: return item + old_count = len(item.children) item.children = [ child for child in item.children if child.can_expand or content_filter(child) ] + item.not_shown += old_count - len(item.children) return item @@ -119,7 +128,13 @@ async def async_resolve_media(hass: HomeAssistant, media_content_id: str) -> Pla """Get info to play media.""" if DOMAIN not in hass.data: raise Unresolvable("Media Source not loaded") - return await _get_media_item(hass, media_content_id).async_resolve() + + try: + item = _get_media_item(hass, media_content_id) + except ValueError as err: + raise Unresolvable(str(err)) from err + + return await item.async_resolve() @websocket_api.websocket_command( @@ -147,7 +162,7 @@ async def websocket_browse_media( { vol.Required("type"): "media_source/resolve_media", vol.Required(ATTR_MEDIA_CONTENT_ID): str, - vol.Optional("expires", default=DEFAULT_EXPIRY_TIME): int, + vol.Optional("expires", default=CONTENT_AUTH_EXPIRY_TIME): int, } ) @websocket_api.async_response @@ -157,15 +172,16 @@ async def websocket_resolve_media( """Resolve media.""" try: media = await async_resolve_media(hass, msg["media_content_id"]) - url = media.url except Unresolvable as err: connection.send_error(msg["id"], "resolve_media_failed", str(err)) - else: - if url[0] == "/": - url = async_sign_path( - hass, - quote(url), - timedelta(seconds=msg["expires"]), - ) - - connection.send_result(msg["id"], {"url": url, "mime_type": media.mime_type}) + return + + connection.send_result( + msg["id"], + { + "url": async_process_play_media_url( + hass, media.url, allow_relative_url=True + ), + "mime_type": media.mime_type, + }, + ) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 7213d6ac7a030..66baa0eaa8c58 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -1,21 +1,29 @@ """Local Media Source Implementation.""" from __future__ import annotations +import logging import mimetypes from pathlib import Path +import shutil from aiohttp import web +from aiohttp.web_request import FileField +import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components import http, websocket_api from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.core import HomeAssistant, callback -from homeassistant.util import raise_if_invalid_path +from homeassistant.exceptions import Unauthorized +from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .error import Unresolvable from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia +MAX_UPLOAD_SIZE = 1024 * 1024 * 10 +LOGGER = logging.getLogger(__name__) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -23,6 +31,8 @@ def async_setup(hass: HomeAssistant) -> None: source = LocalSource(hass) hass.data[DOMAIN][DOMAIN] = source hass.http.register_view(LocalMediaView(hass, source)) + hass.http.register_view(UploadMediaView(hass, source)) + websocket_api.async_register_command(hass, websocket_remove_media) class LocalSource(MediaSource): @@ -43,11 +53,10 @@ def async_full_path(self, source_dir_id: str, location: str) -> Path: @callback def async_parse_identifier(self, item: MediaSourceItem) -> tuple[str, str]: """Parse identifier.""" - if not item.identifier: - # Empty source_dir_id and location - return "", "" + if item.domain != DOMAIN: + raise Unresolvable("Unknown domain.") - source_dir_id, location = item.identifier.split("/", 1) + source_dir_id, _, location = item.identifier.partition("/") if source_dir_id not in self.hass.config.media_dirs: raise Unresolvable("Unknown source directory.") @@ -61,36 +70,39 @@ def async_parse_identifier(self, item: MediaSourceItem) -> tuple[str, str]: async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" source_dir_id, location = self.async_parse_identifier(item) - if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs: - raise Unresolvable("Unknown source directory.") - - mime_type, _ = mimetypes.guess_type( - str(self.async_full_path(source_dir_id, location)) - ) + path = self.async_full_path(source_dir_id, location) + mime_type, _ = mimetypes.guess_type(str(path)) assert isinstance(mime_type, str) return PlayMedia(f"/media/{item.identifier}", mime_type) async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media.""" - try: - source_dir_id, location = self.async_parse_identifier(item) - except Unresolvable as err: - raise BrowseError(str(err)) from err + if item.identifier: + try: + source_dir_id, location = self.async_parse_identifier(item) + except Unresolvable as err: + raise BrowseError(str(err)) from err + + else: + source_dir_id, location = None, "" result = await self.hass.async_add_executor_job( self._browse_media, source_dir_id, location ) + return result - def _browse_media(self, source_dir_id: str, location: str) -> BrowseMediaSource: + def _browse_media( + self, source_dir_id: str | None, location: str + ) -> BrowseMediaSource: """Browse media.""" # If only one media dir is configured, use that as the local media root - if source_dir_id == "" and len(self.hass.config.media_dirs) == 1: + if source_dir_id is None and len(self.hass.config.media_dirs) == 1: source_dir_id = list(self.hass.config.media_dirs)[0] # Multiple folder, root is requested - if source_dir_id == "": + if source_dir_id is None: if location: raise BrowseError("Folder not found.") @@ -178,7 +190,7 @@ def _build_item_response( return media -class LocalMediaView(HomeAssistantView): +class LocalMediaView(http.HomeAssistantView): """ Local Media Finder View. @@ -217,3 +229,137 @@ async def get( raise web.HTTPNotFound() return web.FileResponse(media_path) + + +class UploadMediaView(http.HomeAssistantView): + """View to upload images.""" + + url = "/api/media_source/local_source/upload" + name = "api:media_source:local_source:upload" + + def __init__(self, hass: HomeAssistant, source: LocalSource) -> None: + """Initialize the media view.""" + self.hass = hass + self.source = source + self.schema = vol.Schema( + { + "media_content_id": str, + "file": FileField, + } + ) + + async def post(self, request: web.Request) -> web.Response: + """Handle upload.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + + # Increase max payload + request._client_max_size = MAX_UPLOAD_SIZE # pylint: disable=protected-access + + try: + data = self.schema(dict(await request.post())) + except vol.Invalid as err: + LOGGER.error("Received invalid upload data: %s", err) + raise web.HTTPBadRequest() from err + + try: + item = MediaSourceItem.from_uri(self.hass, data["media_content_id"]) + except ValueError as err: + LOGGER.error("Received invalid upload data: %s", err) + raise web.HTTPBadRequest() from err + + try: + source_dir_id, location = self.source.async_parse_identifier(item) + except Unresolvable as err: + LOGGER.error("Invalid local source ID") + raise web.HTTPBadRequest() from err + + uploaded_file: FileField = data["file"] + + if not uploaded_file.content_type.startswith(("image/", "video/", "audio/")): + LOGGER.error("Content type not allowed") + raise vol.Invalid("Only images and video are allowed") + + try: + raise_if_invalid_filename(uploaded_file.filename) + except ValueError as err: + LOGGER.error("Invalid filename") + raise web.HTTPBadRequest() from err + + try: + await self.hass.async_add_executor_job( + self._move_file, + self.source.async_full_path(source_dir_id, location), + uploaded_file, + ) + except ValueError as err: + LOGGER.error("Moving upload failed: %s", err) + raise web.HTTPBadRequest() from err + + return self.json( + {"media_content_id": f"{data['media_content_id']}/{uploaded_file.filename}"} + ) + + def _move_file( # pylint: disable=no-self-use + self, target_dir: Path, uploaded_file: FileField + ) -> None: + """Move file to target.""" + if not target_dir.is_dir(): + raise ValueError("Target is not an existing directory") + + target_path = target_dir / uploaded_file.filename + + target_path.relative_to(target_dir) + raise_if_invalid_path(str(target_path)) + + with target_path.open("wb") as target_fp: + shutil.copyfileobj(uploaded_file.file, target_fp) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "media_source/local_source/remove", + vol.Required("media_content_id"): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_remove_media( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Remove media.""" + try: + item = MediaSourceItem.from_uri(hass, msg["media_content_id"]) + except ValueError as err: + connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) + return + + source: LocalSource = hass.data[DOMAIN][DOMAIN] + + try: + source_dir_id, location = source.async_parse_identifier(item) + except Unresolvable as err: + connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) + return + + item_path = source.async_full_path(source_dir_id, location) + + def _do_delete() -> tuple[str, str] | None: + if not item_path.exists(): + return websocket_api.ERR_NOT_FOUND, "Path does not exist" + + if not item_path.is_file(): + return websocket_api.ERR_NOT_SUPPORTED, "Path is not a file" + + item_path.unlink() + return None + + try: + error = await hass.async_add_executor_job(_do_delete) + except OSError as err: + error = (websocket_api.ERR_UNKNOWN_ERROR, str(err)) + + if error: + connection.send_error(msg["id"], *error) + else: + connection.send_result(msg["id"]) diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 8ebf87b98d595..ceb57ef1fb4f2 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -64,19 +64,22 @@ async def async_browse(self) -> BrowseMediaSource: can_expand=True, children_media_class=MEDIA_CLASS_APP, ) - base.children = [ - BrowseMediaSource( - domain=source.domain, - identifier=None, - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APP, - thumbnail=f"https://brands.home-assistant.io/_/{source.domain}/logo.png", - title=source.name, - can_play=False, - can_expand=True, - ) - for source in self.hass.data[DOMAIN].values() - ] + base.children = sorted( + ( + BrowseMediaSource( + domain=source.domain, + identifier=None, + media_class=MEDIA_CLASS_APP, + media_content_type=MEDIA_TYPE_APP, + thumbnail=f"https://brands.home-assistant.io/_/{source.domain}/logo.png", + title=source.name, + can_play=False, + can_expand=True, + ) + for source in self.hass.data[DOMAIN].values() + ), + key=lambda item: item.title, + ) return base return await self.async_media_source().async_browse_media(self) diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 4171322400a68..63007f88bbbaf 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": ["pymediaroom==0.6.4.1"], "codeowners": ["@dgomes"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymediaroom"] } diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 355f4c9058b39..2f209667dafc4 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/melcloud", "requirements": ["pymelcloud==2.5.6"], "codeowners": ["@vilppuvuorinen"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pymelcloud"] } diff --git a/homeassistant/components/melcloud/translations/el.json b/homeassistant/components/melcloud/translations/el.json new file mode 100644 index 0000000000000..ddd40e10d825a --- /dev/null +++ b/homeassistant/components/melcloud/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 MELCloud \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf email. \u03a4\u03bf \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03bd\u03b5\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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf MELCloud.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/it.json b/homeassistant/components/melcloud/translations/it.json index c76f2fd9cf260..df40631e6d99d 100644 --- a/homeassistant/components/melcloud/translations/it.json +++ b/homeassistant/components/melcloud/translations/it.json @@ -15,7 +15,7 @@ "username": "Email" }, "description": "Connettiti utilizzando il tuo account MELCloud.", - "title": "Connettersi a MELCloud" + "title": "Connettiti a MELCloud" } } } diff --git a/homeassistant/components/melcloud/translations/pt-BR.json b/homeassistant/components/melcloud/translations/pt-BR.json new file mode 100644 index 0000000000000..2982f4997fe2b --- /dev/null +++ b/homeassistant/components/melcloud/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Email" + }, + "description": "Conecte-se usando sua conta MELCloud.", + "title": "Conecte-se ao MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/sk.json b/homeassistant/components/melcloud/translations/sk.json new file mode 100644 index 0000000000000..c043ef9ff19d2 --- /dev/null +++ b/homeassistant/components/melcloud/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index d3b4f95a82ebd..2839f74a5cd07 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": ["py-melissa-climate==2.1.4"], "codeowners": ["@kennedyshead"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["melissa"] } diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index 9e38e9d724e96..f3278956911f3 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": ["messagebird==1.2.0"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["messagebird"] } diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index b6c3e565dc0f6..1ce70f25ea539 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/met", "requirements": ["pyMetno==0.9.0"], "codeowners": ["@danielhiversen", "@thimic"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["metno"] } diff --git a/homeassistant/components/met/translations/cs.json b/homeassistant/components/met/translations/cs.json index 6ee3d3c4dc138..0a3c7ec802e28 100644 --- a/homeassistant/components/met/translations/cs.json +++ b/homeassistant/components/met/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "V konfiguraci Home Assistant nejsou nastaveny \u017e\u00e1dn\u00e9 domovsk\u00e9 sou\u0159adnice" + }, "error": { "already_configured": "Slu\u017eba je ji\u017e nastavena" }, diff --git a/homeassistant/components/met/translations/el.json b/homeassistant/components/met/translations/el.json index dd50a5ac78442..9c0d532f29864 100644 --- a/homeassistant/components/met/translations/el.json +++ b/homeassistant/components/met/translations/el.json @@ -2,6 +2,21 @@ "config": { "abort": { "no_home": "\u0394\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03ba\u03b1\u03c4\u03bf\u03b9\u03ba\u03af\u03b1\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Home Assistant" + }, + "error": { + "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" + }, + "step": { + "user": { + "data": { + "elevation": "\u0391\u03bd\u03cd\u03c8\u03c9\u03c3\u03b7", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "Meteorologisk institutt", + "title": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" + } } } } \ No newline at end of file diff --git a/homeassistant/components/met/translations/pt-BR.json b/homeassistant/components/met/translations/pt-BR.json index ac85a893c215d..d87561136a65f 100644 --- a/homeassistant/components/met/translations/pt-BR.json +++ b/homeassistant/components/met/translations/pt-BR.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "no_home": "Nenhuma coordenada de casa est\u00e1 definida na configura\u00e7\u00e3o do Home Assistant" + }, + "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met/translations/sk.json b/homeassistant/components/met/translations/sk.json new file mode 100644 index 0000000000000..55d4920e30abc --- /dev/null +++ b/homeassistant/components/met/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "no_home": "V konfigur\u00e1cii Home Assistant nie s\u00fa nastaven\u00e9 \u017eiadne dom\u00e1ce s\u00faradnice" + }, + "error": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "user": { + "data": { + "elevation": "Nadmorsk\u00e1 v\u00fd\u0161ka", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + }, + "description": "Meteorologick\u00fd \u00fastav", + "title": "Umiestnenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json index 36cc905eabf45..ad91ce528cc2b 100644 --- a/homeassistant/components/met_eireann/manifest.json +++ b/homeassistant/components/met_eireann/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/met_eireann", "requirements": ["pyMetEireann==2021.8.0"], "codeowners": ["@DylanGore"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["meteireann"] } diff --git a/homeassistant/components/met_eireann/translations/el.json b/homeassistant/components/met_eireann/translations/el.json index 65335fe7c2be6..7d30d65b9181f 100644 --- a/homeassistant/components/met_eireann/translations/el.json +++ b/homeassistant/components/met_eireann/translations/el.json @@ -1,8 +1,18 @@ { "config": { + "error": { + "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" + }, "step": { "user": { - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b1\u03c0\u03cc \u03c4\u03bf Met \u00c9ireann Public Weather Forecast API" + "data": { + "elevation": "\u0391\u03bd\u03cd\u03c8\u03c9\u03c3\u03b7", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b1\u03c0\u03cc \u03c4\u03bf Met \u00c9ireann Public Weather Forecast API", + "title": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" } } } diff --git a/homeassistant/components/met_eireann/translations/pt-BR.json b/homeassistant/components/met_eireann/translations/pt-BR.json new file mode 100644 index 0000000000000..d72a8d1ccfd24 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "elevation": "Eleva\u00e7\u00e3o", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Insira sua localiza\u00e7\u00e3o para usar os dados meteorol\u00f3gicos da API de previs\u00e3o meteorol\u00f3gica p\u00fablica do Met \u00c9ireann", + "title": "Localiza\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/sk.json b/homeassistant/components/met_eireann/translations/sk.json new file mode 100644 index 0000000000000..492ab052d9a5d --- /dev/null +++ b/homeassistant/components/met_eireann/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Nadmorsk\u00e1 v\u00fd\u0161ka", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + }, + "title": "Umiestnenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 2f47aee9c02fc..bbc3b16875da8 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -186,6 +186,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index e7d1c4bd64a52..cfdd62933c02b 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": ["meteofrance-api==1.0.2"], "codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["meteofrance_api"] } diff --git a/homeassistant/components/meteo_france/translations/el.json b/homeassistant/components/meteo_france/translations/el.json index a1adaa91c1824..3dfad5a493df0 100644 --- a/homeassistant/components/meteo_france/translations/el.json +++ b/homeassistant/components/meteo_france/translations/el.json @@ -1,5 +1,9 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "error": { "empty": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b1\u03c0\u03bf\u03c4\u03ad\u03bb\u03b5\u03c3\u03bc\u03b1 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03c0\u03cc\u03bb\u03b7\u03c2: \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u03c0\u03cc\u03bb\u03b7\u03c2" }, @@ -19,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b5\u03c8\u03b7\u03c2" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/pt-BR.json b/homeassistant/components/meteo_france/translations/pt-BR.json index f23bdb1379d8c..456c6eef17cf0 100644 --- a/homeassistant/components/meteo_france/translations/pt-BR.json +++ b/homeassistant/components/meteo_france/translations/pt-BR.json @@ -1,13 +1,34 @@ { "config": { "abort": { - "already_configured": "Cidade j\u00e1 configurada", - "unknown": "Erro desconhecido: tente novamente mais tarde" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "unknown": "Erro inesperado" + }, + "error": { + "empty": "Nenhum resultado na pesquisa da cidade: verifique o campo da cidade" }, "step": { + "cities": { + "data": { + "city": "Cidade" + }, + "description": "Escolha sua cidade na lista", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "Cidade" + }, + "description": "Insira o c\u00f3digo postal (somente para a Fran\u00e7a, recomendado) ou o nome da cidade", + "title": "M\u00e9t\u00e9o-France" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Modo de previs\u00e3o" } } } diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index ffdd7d8f49d2a..35333f6ea0154 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": ["meteoalertapi==0.2.0"], "codeowners": ["@rolfberkenbosch"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["meteoalertapi"] } diff --git a/homeassistant/components/meteoclimatic/manifest.json b/homeassistant/components/meteoclimatic/manifest.json index 71174f216a474..6c573b0c0d46e 100644 --- a/homeassistant/components/meteoclimatic/manifest.json +++ b/homeassistant/components/meteoclimatic/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@adrianmo" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["meteoclimatic"] } diff --git a/homeassistant/components/meteoclimatic/translations/cs.json b/homeassistant/components/meteoclimatic/translations/cs.json new file mode 100644 index 0000000000000..3b814303e6958 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/el.json b/homeassistant/components/meteoclimatic/translations/el.json index b852b1f619278..085e6fe164318 100644 --- a/homeassistant/components/meteoclimatic/translations/el.json +++ b/homeassistant/components/meteoclimatic/translations/el.json @@ -1,10 +1,19 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "not_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" + }, "step": { "user": { "data": { "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd" - } + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd Meteoclimatic (\u03c0.\u03c7. ESCAT4300000043206B)", + "title": "Meteoclimatic" } } } diff --git a/homeassistant/components/meteoclimatic/translations/pt-BR.json b/homeassistant/components/meteoclimatic/translations/pt-BR.json new file mode 100644 index 0000000000000..c81109b8939dd --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" + }, + "error": { + "not_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "code": "C\u00f3digo da esta\u00e7\u00e3o" + }, + "description": "Digite o c\u00f3digo da esta\u00e7\u00e3o Meteoclim\u00e1tica (por exemplo, ESCAT4300000043206B)", + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index db6832b04b454..d38d2d8cffeb1 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -5,5 +5,6 @@ "requirements": ["datapoint==0.9.8"], "codeowners": ["@MrHarcombe"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["datapoint"] } diff --git a/homeassistant/components/metoffice/translations/el.json b/homeassistant/components/metoffice/translations/el.json new file mode 100644 index 0000000000000..9170cdf481f3d --- /dev/null +++ b/homeassistant/components/metoffice/translations/el.json @@ -0,0 +1,22 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + }, + "description": "\u03a4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03bb\u03b7\u03c3\u03b9\u03ad\u03c3\u03c4\u03b5\u03c1\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b5\u03c9\u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf UK Met Office" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/pt-BR.json b/homeassistant/components/metoffice/translations/pt-BR.json new file mode 100644 index 0000000000000..3e0fc4b79d358 --- /dev/null +++ b/homeassistant/components/metoffice/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "A latitude e a longitude ser\u00e3o usadas para encontrar a esta\u00e7\u00e3o meteorol\u00f3gica mais pr\u00f3xima.", + "title": "Conecte-se ao Met Office do Reino Unido" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/sk.json b/homeassistant/components/metoffice/translations/sk.json new file mode 100644 index 0000000000000..abb3969f6b4ac --- /dev/null +++ b/homeassistant/components/metoffice/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 8ac5f3876351d..7aaea34ea608b 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": ["mficlient==0.3.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["mficlient"] } diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index aa2271f2dd4d5..349fba8c7a215 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": ["pmsensor==0.4"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pmsensor"] } diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 299209e9b9760..ec393125d24f3 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": ["pycsspeechtts==1.0.4"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pycsspeechtts"] } diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 611cf57e56ba2..59902335d4765 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -58,6 +58,7 @@ "hr-hr", "hu-hu", "id-id", + "is-is", "it-it", "ja-jp", "ko-kr", diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index 9242428ebf7a9..eea4b2b82fe66 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": ["bluepy==1.3.0", "miflora==0.7.2"], "codeowners": ["@danielhiversen", "@basnijholt"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["btlewrap", "miflora"] } diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index eff9d26103d4f..769db5898c2a4 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": ["librouteros==3.2.0"], "codeowners": ["@engrbm87"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["librouteros"] } diff --git a/homeassistant/components/mikrotik/translations/el.json b/homeassistant/components/mikrotik/translations/el.json index 588841a7be812..4faca6d756e2b 100644 --- a/homeassistant/components/mikrotik/translations/el.json +++ b/homeassistant/components/mikrotik/translations/el.json @@ -1,13 +1,24 @@ { "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", "name_exists": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9" }, "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u03a7\u03c1\u03ae\u03c3\u03b7 ssl" - } + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Mikrotik" } } }, @@ -15,6 +26,7 @@ "step": { "device_tracker": { "data": { + "arp_ping": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 ARP ping", "detection_time": "\u0395\u03be\u03b5\u03c4\u03ac\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03c4\u03bf \u03c3\u03c0\u03af\u03c4\u03b9", "force_dhcp": "\u0391\u03bd\u03b1\u03b3\u03ba\u03b1\u03c3\u03c4\u03b9\u03ba\u03ae \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 DHCP" } diff --git a/homeassistant/components/mikrotik/translations/pt-BR.json b/homeassistant/components/mikrotik/translations/pt-BR.json index 2a013ba477235..0fb66a063bddd 100644 --- a/homeassistant/components/mikrotik/translations/pt-BR.json +++ b/homeassistant/components/mikrotik/translations/pt-BR.json @@ -1,21 +1,36 @@ { "config": { "abort": { - "already_configured": "Mikrotik j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Conex\u00e3o malsucedida", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "name_exists": "O nome j\u00e1 existe" }, "step": { "user": { "data": { + "host": "Nome do host", "name": "Nome", + "password": "Senha", + "port": "Porta", "username": "Usu\u00e1rio", "verify_ssl": "Usar SSL" }, "title": "Configurar roteador Mikrotik" } } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Habilitar ping ARP", + "detection_time": "Considere o tempo para definir em casa", + "force_dhcp": "For\u00e7ar varredura usando DHCP" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/sk.json b/homeassistant/components/mikrotik/translations/sk.json new file mode 100644 index 0000000000000..6f753f2096654 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 897afc9666568..4a06e597c3608 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -184,7 +184,7 @@ def _update_attr(self, heater): self._attr_target_temperature = heater.set_temp self._attr_current_temperature = heater.current_temp self._attr_fan_mode = FAN_ON if heater.fan_status == 1 else HVAC_MODE_OFF - if heater.is_gen1 or heater.is_heating == 1: + if heater.is_heating == 1: self._attr_hvac_action = CURRENT_HVAC_HEAT else: self._attr_hvac_action = CURRENT_HVAC_IDLE diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 7cea7118882ad..c2adebae594ed 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -5,5 +5,6 @@ "requirements": ["millheater==0.9.0", "mill-local==0.1.1"], "codeowners": ["@danielhiversen"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["mill", "mill_local"] } diff --git a/homeassistant/components/mill/translations/el.json b/homeassistant/components/mill/translations/el.json index ed475f6c0dd1a..18743aada0a21 100644 --- a/homeassistant/components/mill/translations/el.json +++ b/homeassistant/components/mill/translations/el.json @@ -1,12 +1,29 @@ { "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { + "cloud": { + "data": { + "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" + } + }, "local": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2." }, "user": { "data": { - "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\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" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b5\u03c2 3\u03b7\u03c2 \u03b3\u03b5\u03bd\u03b9\u03ac\u03c2" } diff --git a/homeassistant/components/mill/translations/nb.json b/homeassistant/components/mill/translations/nb.json new file mode 100644 index 0000000000000..cb56d003e525e --- /dev/null +++ b/homeassistant/components/mill/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/pt-BR.json b/homeassistant/components/mill/translations/pt-BR.json new file mode 100644 index 0000000000000..8d90531191aa6 --- /dev/null +++ b/homeassistant/components/mill/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "cloud": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "local": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Endere\u00e7o IP local do dispositivo." + }, + "user": { + "data": { + "connection_type": "Selecione o tipo de conex\u00e3o", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Selecione o tipo de conex\u00e3o. Local requer aquecedores de 3\u00aa gera\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 99a5ff3a463d4..b74b2e2bf2aeb 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aiodns==3.0.0", "getmac==0.8.2", "mcstatus==6.0.0"], "codeowners": ["@elmurato"], "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["dnspython", "mcstatus"] } diff --git a/homeassistant/components/minecraft_server/translations/el.json b/homeassistant/components/minecraft_server/translations/el.json index d69c70c4845f0..26e755cad5bf4 100644 --- a/homeassistant/components/minecraft_server/translations/el.json +++ b/homeassistant/components/minecraft_server/translations/el.json @@ -1,9 +1,22 @@ { "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\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b9 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 1.7 \u03c4\u03bf\u03c5 Minecraft \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2.", "invalid_ip": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03ba\u03c5\u03c1\u03b7 (\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af). \u0394\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "invalid_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03c5\u03bc\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc 1024 \u03ad\u03c9\u03c2 65535. \u0394\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + }, + "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" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Minecraft \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Minecraft" + } } } } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/pt-BR.json b/homeassistant/components/minecraft_server/translations/pt-BR.json index 5aa2fc3609a4a..d71651e98e856 100644 --- a/homeassistant/components/minecraft_server/translations/pt-BR.json +++ b/homeassistant/components/minecraft_server/translations/pt-BR.json @@ -1,7 +1,22 @@ { "config": { "abort": { - "already_configured": "O host j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar ao servidor. Verifique o host e a porta e tente novamente. Verifique tamb\u00e9m se voc\u00ea est\u00e1 executando pelo menos a vers\u00e3o 1.7 do Minecraft em seu servidor.", + "invalid_ip": "O endere\u00e7o IP \u00e9 inv\u00e1lido (o endere\u00e7o MAC n\u00e3o p\u00f4de ser determinado). Corrija-o e tente novamente.", + "invalid_port": "A porta deve estar no intervalo de 1024 a 65535. Corrija-a e tente novamente." + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Configure sua inst\u00e2ncia do Minecraft Server para permitir o monitoramento.", + "title": "Vincule seu servidor Minecraft" + } } } } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/sk.json b/homeassistant/components/minecraft_server/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index ba5ba4cd0a8e3..f89db2346d98d 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": ["minio==5.0.10"], "codeowners": ["@tkislan"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["minio"] } diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index f0465315cefa0..07121b3695bbd 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": ["mitemp_bt==0.0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["btlewrap", "mitemp_bt"] } diff --git a/homeassistant/components/mjpeg/__init__.py b/homeassistant/components/mjpeg/__init__.py index 3e7469cff004e..632156b7adc31 100644 --- a/homeassistant/components/mjpeg/__init__.py +++ b/homeassistant/components/mjpeg/__init__.py @@ -1 +1,42 @@ -"""The mjpeg component.""" +"""The MJPEG IP Camera integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .camera import MjpegCamera +from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, PLATFORMS +from .util import filter_urllib3_logging + +__all__ = [ + "CONF_MJPEG_URL", + "CONF_STILL_IMAGE_URL", + "MjpegCamera", + "filter_urllib3_logging", +] + + +def setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the MJPEG IP Camera integration.""" + filter_urllib3_logging() + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + # Reload entry when its updated. + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload the config entry when it changed.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 1d60206f2d8c8..69588c1b670e0 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -2,16 +2,18 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from contextlib import closing -import logging import aiohttp +from aiohttp import web import async_timeout import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -27,13 +29,12 @@ async_aiohttp_proxy_web, async_get_clientsession, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) +from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN, LOGGER -CONF_MJPEG_URL = "mjpeg_url" -CONF_STILL_IMAGE_URL = "still_image_url" CONTENT_TYPE_HEADER = "Content-Type" DEFAULT_NAME = "Mjpeg Camera" @@ -47,7 +48,7 @@ [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } @@ -60,22 +61,53 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a MJPEG IP Camera.""" - filter_urllib3_logging() + """Set up the MJPEG IP camera from platform.""" + LOGGER.warning( + "Configuration of the MJPEG IP Camera platform in YAML is deprecated " + "and will be removed in Home Assistant 2022.5; Your existing " + "configuration has been imported into the UI automatically and can be " + "safely removed from your configuration.yaml file" + ) if discovery_info: config = PLATFORM_SCHEMA(discovery_info) - async_add_entities([MjpegCamera(config)]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -def filter_urllib3_logging(): - """Filter header errors from urllib3 due to a urllib3 bug.""" - urllib3_logger = logging.getLogger("urllib3.connectionpool") - if not any(isinstance(x, NoHeaderErrorFilter) for x in urllib3_logger.filters): - urllib3_logger.addFilter(NoHeaderErrorFilter()) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a MJPEG IP Camera based on a config entry.""" + async_add_entities( + [ + MjpegCamera( + name=entry.title, + authentication=entry.options[CONF_AUTHENTICATION], + username=entry.options.get(CONF_USERNAME), + password=entry.options[CONF_PASSWORD], + mjpeg_url=entry.options[CONF_MJPEG_URL], + still_image_url=entry.options.get(CONF_STILL_IMAGE_URL), + verify_ssl=entry.options[CONF_VERIFY_SSL], + unique_id=entry.entry_id, + device_info=DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + ), + ) + ] + ) -def extract_image_from_mjpeg(stream): +def extract_image_from_mjpeg(stream: Iterable[bytes]) -> bytes | None: """Take in a MJPEG stream object, return the jpg from it.""" data = b"" @@ -93,19 +125,33 @@ def extract_image_from_mjpeg(stream): return data[jpg_start : jpg_end + 2] + return None + class MjpegCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" - def __init__(self, device_info): + def __init__( + self, + *, + name: str, + mjpeg_url: str, + still_image_url: str | None, + authentication: str | None = None, + username: str | None = None, + password: str = "", + verify_ssl: bool = True, + unique_id: str | None = None, + device_info: DeviceInfo | None = None, + ) -> None: """Initialize a MJPEG camera.""" super().__init__() - self._name = device_info.get(CONF_NAME) - self._authentication = device_info.get(CONF_AUTHENTICATION) - self._username = device_info.get(CONF_USERNAME) - self._password = device_info.get(CONF_PASSWORD) - self._mjpeg_url = device_info[CONF_MJPEG_URL] - self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + self._attr_name = name + self._authentication = authentication + self._username = username + self._password = password + self._mjpeg_url = mjpeg_url + self._still_image_url = still_image_url self._auth = None if ( @@ -114,7 +160,12 @@ def __init__(self, device_info): and self._authentication == HTTP_BASIC_AUTHENTICATION ): self._auth = aiohttp.BasicAuth(self._username, password=self._password) - self._verify_ssl = device_info.get(CONF_VERIFY_SSL) + self._verify_ssl = verify_ssl + + if unique_id is not None: + self._attr_unique_id = unique_id + if device_info is not None: + self._attr_device_info = device_info async def async_camera_image( self, width: int | None = None, height: int | None = None @@ -137,10 +188,10 @@ async def async_camera_image( return image except asyncio.TimeoutError: - _LOGGER.error("Timeout getting camera image from %s", self._name) + LOGGER.error("Timeout getting camera image from %s", self.name) except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) + LOGGER.error("Error getting new camera image from %s: %s", self.name, err) return None @@ -168,7 +219,9 @@ def camera_image( with closing(req) as response: return extract_image_from_mjpeg(response.iter_content(102400)) - async def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse | None: """Generate an HTTP MJPEG stream from the camera.""" # aiohttp don't support DigestAuth -> Fallback if self._authentication == HTTP_DIGEST_AUTHENTICATION: @@ -179,16 +232,3 @@ async def handle_async_mjpeg_stream(self, request): stream_coro = websession.get(self._mjpeg_url, auth=self._auth) return await async_aiohttp_proxy_web(self.hass, request, stream_coro) - - @property - def name(self): - """Return the name of this camera.""" - return self._name - - -class NoHeaderErrorFilter(logging.Filter): - """Filter out urllib3 Header Parsing Errors due to a urllib3 bug.""" - - def filter(self, record): - """Filter out Header Parsing Errors.""" - return "Failed to parse headers" not in record.getMessage() diff --git a/homeassistant/components/mjpeg/config_flow.py b/homeassistant/components/mjpeg/config_flow.py new file mode 100644 index 0000000000000..2eecdd84d7889 --- /dev/null +++ b/homeassistant/components/mjpeg/config_flow.py @@ -0,0 +1,240 @@ +"""Config flow to configure the MJPEG IP Camera integration.""" +from __future__ import annotations + +from http import HTTPStatus +from types import MappingProxyType +from typing import Any + +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from requests.exceptions import HTTPError, Timeout +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN, LOGGER + + +@callback +def async_get_schema( + defaults: dict[str, Any] | MappingProxyType[str, Any], show_name: bool = False +) -> vol.Schema: + """Return MJPEG IP Camera schema.""" + schema = { + vol.Required(CONF_MJPEG_URL, default=defaults.get(CONF_MJPEG_URL)): str, + vol.Optional( + CONF_STILL_IMAGE_URL, + description={"suggested_value": defaults.get(CONF_STILL_IMAGE_URL)}, + ): str, + vol.Optional( + CONF_USERNAME, + description={"suggested_value": defaults.get(CONF_USERNAME)}, + ): str, + vol.Optional( + CONF_PASSWORD, + default=defaults.get(CONF_PASSWORD, ""), + ): str, + vol.Optional( + CONF_VERIFY_SSL, + default=defaults.get(CONF_VERIFY_SSL, True), + ): bool, + } + + if show_name: + schema = { + vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME)): str, + **schema, + } + + return vol.Schema(schema) + + +def validate_url( + url: str, + username: str | None, + password: str, + verify_ssl: bool, + authentication: str = HTTP_BASIC_AUTHENTICATION, +) -> str: + """Test if the given setting works as expected.""" + auth: HTTPDigestAuth | HTTPBasicAuth | None = None + if username and password: + if authentication == HTTP_DIGEST_AUTHENTICATION: + auth = HTTPDigestAuth(username, password) + else: + auth = HTTPBasicAuth(username, password) + + response = requests.get( + url, + auth=auth, + stream=True, + timeout=10, + verify=verify_ssl, + ) + + if response.status_code == HTTPStatus.UNAUTHORIZED: + # If unauthorized, try again using digest auth + if authentication == HTTP_BASIC_AUTHENTICATION: + return validate_url( + url, username, password, verify_ssl, HTTP_DIGEST_AUTHENTICATION + ) + raise InvalidAuth + + response.raise_for_status() + response.close() + + return authentication + + +async def async_validate_input( + hass: HomeAssistant, user_input: dict[str, Any] +) -> tuple[dict[str, str], str]: + """Manage MJPEG IP Camera options.""" + errors = {} + field = "base" + authentication = HTTP_BASIC_AUTHENTICATION + try: + for field in (CONF_MJPEG_URL, CONF_STILL_IMAGE_URL): + if not (url := user_input.get(field)): + continue + authentication = await hass.async_add_executor_job( + validate_url, + url, + user_input.get(CONF_USERNAME), + user_input[CONF_PASSWORD], + user_input[CONF_VERIFY_SSL], + ) + except InvalidAuth: + errors["username"] = "invalid_auth" + except (OSError, HTTPError, Timeout): + LOGGER.exception("Cannot connect to %s", user_input[CONF_MJPEG_URL]) + errors[field] = "cannot_connect" + + return (errors, authentication) + + +class MJPEGFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for MJPEG IP Camera.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> MJPEGOptionsFlowHandler: + """Get the options flow for this handler.""" + return MJPEGOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + + if user_input is not None: + errors, authentication = await async_validate_input(self.hass, user_input) + if not errors: + self._async_abort_entries_match( + {CONF_MJPEG_URL: user_input[CONF_MJPEG_URL]} + ) + + # Storing data in option, to allow for changing them later + # using an options flow. + return self.async_create_entry( + title=user_input.get(CONF_NAME, user_input[CONF_MJPEG_URL]), + data={}, + options={ + CONF_AUTHENTICATION: authentication, + CONF_MJPEG_URL: user_input[CONF_MJPEG_URL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL), + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + else: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=async_get_schema(user_input, show_name=True), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Handle a flow initialized by importing a config.""" + self._async_abort_entries_match({CONF_MJPEG_URL: config[CONF_MJPEG_URL]}) + return self.async_create_entry( + title=config[CONF_NAME], + data={}, + options={ + CONF_AUTHENTICATION: config[CONF_AUTHENTICATION], + CONF_MJPEG_URL: config[CONF_MJPEG_URL], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_STILL_IMAGE_URL: config.get(CONF_STILL_IMAGE_URL), + CONF_USERNAME: config.get(CONF_USERNAME), + CONF_VERIFY_SSL: config[CONF_VERIFY_SSL], + }, + ) + + +class MJPEGOptionsFlowHandler(OptionsFlow): + """Handle MJPEG IP Camera options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize MJPEG IP Camera options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage MJPEG IP Camera options.""" + errors: dict[str, str] = {} + + if user_input is not None: + errors, authentication = await async_validate_input(self.hass, user_input) + if not errors: + for entry in self.hass.config_entries.async_entries(DOMAIN): + if ( + entry.entry_id != self.config_entry.entry_id + and entry.options[CONF_MJPEG_URL] == user_input[CONF_MJPEG_URL] + ): + errors = {CONF_MJPEG_URL: "already_configured"} + + if not errors: + return self.async_create_entry( + title=user_input.get(CONF_NAME, user_input[CONF_MJPEG_URL]), + data={ + CONF_AUTHENTICATION: authentication, + CONF_MJPEG_URL: user_input[CONF_MJPEG_URL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL), + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + else: + user_input = {} + + return self.async_show_form( + step_id="init", + data_schema=async_get_schema(user_input or self.config_entry.options), + errors=errors, + ) + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/mjpeg/const.py b/homeassistant/components/mjpeg/const.py new file mode 100644 index 0000000000000..94cd01676ecbc --- /dev/null +++ b/homeassistant/components/mjpeg/const.py @@ -0,0 +1,14 @@ +"""Constants for the MJPEG integration.""" + +import logging +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "mjpeg" +PLATFORMS: Final = [Platform.CAMERA] + +LOGGER = logging.getLogger(__package__) + +CONF_MJPEG_URL: Final = "mjpeg_url" +CONF_STILL_IMAGE_URL: Final = "still_image_url" diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 88e4cdba35628..02726c3bb3fe1 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -3,5 +3,6 @@ "name": "MJPEG IP Camera", "documentation": "https://www.home-assistant.io/integrations/mjpeg", "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/mjpeg/strings.json b/homeassistant/components/mjpeg/strings.json new file mode 100644 index 0000000000000..73e6a150a09fe --- /dev/null +++ b/homeassistant/components/mjpeg/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "[%key:common::config_flow::data::name%]", + "password": "[%key:common::config_flow::data::password%]", + "still_image_url": "Still Image URL", + "username": "[%key:common::config_flow::data::username%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "[%key:common::config_flow::data::name%]", + "password": "[%key:common::config_flow::data::password%]", + "still_image_url": "Still Image URL", + "username": "[%key:common::config_flow::data::username%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/mjpeg/translations/bg.json b/homeassistant/components/mjpeg/translations/bg.json new file mode 100644 index 0000000000000..0e88f5081913d --- /dev/null +++ b/homeassistant/components/mjpeg/translations/bg.json @@ -0,0 +1,38 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "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", + "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": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/ca.json b/homeassistant/components/mjpeg/translations/ca.json new file mode 100644 index 0000000000000..5d94ca078734a --- /dev/null +++ b/homeassistant/components/mjpeg/translations/ca.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", + "password": "Contrasenya", + "still_image_url": "URL d'imatge fixa", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", + "password": "Contrasenya", + "still_image_url": "URL d'imatge fixa", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json new file mode 100644 index 0000000000000..00616fbdd504a --- /dev/null +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "name": "Jm\u00e9no" + } + } + } + }, + "options": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "init": { + "data": { + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/de.json b/homeassistant/components/mjpeg/translations/de.json new file mode 100644 index 0000000000000..3023dd5bdf170 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG-URL", + "name": "Name", + "password": "Passwort", + "still_image_url": "Standbild-URL", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + }, + "options": { + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG-URL", + "name": "Name", + "password": "Passwort", + "still_image_url": "Standbild-URL", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/el.json b/homeassistant/components/mjpeg/translations/el.json new file mode 100644 index 0000000000000..5660d4fcf6c5e --- /dev/null +++ b/homeassistant/components/mjpeg/translations/el.json @@ -0,0 +1,42 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MJPEG URL", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "still_image_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2", + "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" + } + } + } + }, + "options": { + "error": { + "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", + "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": { + "init": { + "data": { + "mjpeg_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MJPEG URL", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "still_image_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2", + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/en.json b/homeassistant/components/mjpeg/translations/en.json new file mode 100644 index 0000000000000..e389850a360bc --- /dev/null +++ b/homeassistant/components/mjpeg/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Name", + "password": "Password", + "still_image_url": "Still Image URL", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + } + } + } + }, + "options": { + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Name", + "password": "Password", + "still_image_url": "Still Image URL", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/es.json b/homeassistant/components/mjpeg/translations/es.json new file mode 100644 index 0000000000000..113193e3832ec --- /dev/null +++ b/homeassistant/components/mjpeg/translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya se encuentra configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nombre", + "password": "Contrase\u00f1a", + "still_image_url": "URL de imagen est\u00e1tica", + "username": "Usuario", + "verify_ssl": "Verifique el certificado SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "El dispositivo ya se encuentra configurado", + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nombre", + "password": "Contrase\u00f1a", + "still_image_url": "URL de imagen est\u00e1tica", + "username": "Nombre de Usuario", + "verify_ssl": "Verifique el certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/et.json b/homeassistant/components/mjpeg/translations/et.json new file mode 100644 index 0000000000000..d2aa02fe0f488 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/et.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Nimi", + "password": "Salas\u00f5na", + "still_image_url": "Pildi URL", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL serti" + } + } + } + }, + "options": { + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Nimi", + "password": "Salas\u00f5na", + "still_image_url": "Pildi URL", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL serti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/he.json b/homeassistant/components/mjpeg/translations/he.json new file mode 100644 index 0000000000000..2d53457d2778b --- /dev/null +++ b/homeassistant/components/mjpeg/translations/he.json @@ -0,0 +1,38 @@ +{ + "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", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "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" + } + } + } + }, + "options": { + "error": { + "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" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/hu.json b/homeassistant/components/mjpeg/translations/hu.json new file mode 100644 index 0000000000000..0a87f4848872c --- /dev/null +++ b/homeassistant/components/mjpeg/translations/hu.json @@ -0,0 +1,42 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL-c\u00edme", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "still_image_url": "\u00c1ll\u00f3k\u00e9p URL-c\u00edme", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + }, + "options": { + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL-c\u00edme", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "still_image_url": "\u00c1ll\u00f3k\u00e9p URL-c\u00edme", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/id.json b/homeassistant/components/mjpeg/translations/id.json new file mode 100644 index 0000000000000..d38dc06f74884 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nama", + "password": "Kata Sandi", + "still_image_url": "URL Gambar Diam", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nama", + "password": "Kata Sandi", + "still_image_url": "URL Gambar Diam", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/it.json b/homeassistant/components/mjpeg/translations/it.json new file mode 100644 index 0000000000000..09eab73359b06 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nome", + "password": "Password", + "still_image_url": "URL dell'immagine fissa", + "username": "Nome utente", + "verify_ssl": "Verifica il certificato SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nome", + "password": "Password", + "still_image_url": "URL dell'immagine fissa", + "username": "Nome utente", + "verify_ssl": "Verifica il certificato SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/ja.json b/homeassistant/components/mjpeg/translations/ja.json new file mode 100644 index 0000000000000..622087ad5e50f --- /dev/null +++ b/homeassistant/components/mjpeg/translations/ja.json @@ -0,0 +1,42 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "still_image_url": "\u9759\u6b62\u753b\u306eURL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } + }, + "options": { + "error": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\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" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "still_image_url": "\u9759\u6b62\u753b\u306eURL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/nl.json b/homeassistant/components/mjpeg/translations/nl.json new file mode 100644 index 0000000000000..111756af3e337 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Naam", + "password": "Wachtwoord", + "still_image_url": "Stilstaand beeld URL", + "username": "Gebruikersnaam", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + }, + "options": { + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "Naam", + "password": "Wachtwoord", + "still_image_url": "Stilstaand beeld URL", + "username": "Gebruikersnaam", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/no.json b/homeassistant/components/mjpeg/translations/no.json new file mode 100644 index 0000000000000..cf03121d76191 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL-adresse for MJPEG", + "name": "Navn", + "password": "Passord", + "still_image_url": "URL-adresse for stillbilde", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + }, + "options": { + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL-adresse for MJPEG", + "name": "Navn", + "password": "Passord", + "still_image_url": "URL-adresse for stillbilde", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pl.json b/homeassistant/components/mjpeg/translations/pl.json new file mode 100644 index 0000000000000..701049ceac44b --- /dev/null +++ b/homeassistant/components/mjpeg/translations/pl.json @@ -0,0 +1,42 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "Adres URL dla MJPEG", + "name": "Nazwa", + "password": "Has\u0142o", + "still_image_url": "Adres URL dla obrazu nieruchomego (still image)", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "Adres URL dla MJPEG", + "name": "Nazwa", + "password": "Has\u0142o", + "still_image_url": "Adres URL dla obrazu nieruchomego (still image)", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pt-BR.json b/homeassistant/components/mjpeg/translations/pt-BR.json new file mode 100644 index 0000000000000..f54828ea2246b --- /dev/null +++ b/homeassistant/components/mjpeg/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nome", + "password": "Senha", + "still_image_url": "URL da imagem est\u00e1tica", + "username": "Nome de usu\u00e1rio", + "verify_ssl": "Verificar certificado SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nome", + "password": "Senha", + "still_image_url": "URL da imagem est\u00e1tica", + "username": "Nome de usu\u00e1rio", + "verify_ssl": "Verificar certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/ru.json b/homeassistant/components/mjpeg/translations/ru.json new file mode 100644 index 0000000000000..80e624f3d01d9 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/ru.json @@ -0,0 +1,42 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL-\u0430\u0434\u0440\u0435\u0441 MJPEG", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "still_image_url": "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", + "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" + } + } + } + }, + "options": { + "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_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "init": { + "data": { + "mjpeg_url": "URL-\u0430\u0434\u0440\u0435\u0441 MJPEG", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "still_image_url": "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", + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/sk.json b/homeassistant/components/mjpeg/translations/sk.json new file mode 100644 index 0000000000000..4a2050c2353b5 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/sk.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + }, + "options": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "init": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/tr.json b/homeassistant/components/mjpeg/translations/tr.json new file mode 100644 index 0000000000000..b0c5d3b814dac --- /dev/null +++ b/homeassistant/components/mjpeg/translations/tr.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL'si", + "name": "Ad", + "password": "Parola", + "still_image_url": "Sabit Resim URL'si", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + } + } + } + }, + "options": { + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL'si", + "name": "Ad", + "password": "Parola", + "still_image_url": "Sabit Resim URL'si", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/zh-Hant.json b/homeassistant/components/mjpeg/translations/zh-Hant.json new file mode 100644 index 0000000000000..1416bc3ca0038 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "still_image_url": "\u975c\u614b\u5716\u50cf URL", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + }, + "options": { + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "init": { + "data": { + "mjpeg_url": "MJPEG URL", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "still_image_url": "\u975c\u614b\u5716\u50cf URL", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/util.py b/homeassistant/components/mjpeg/util.py new file mode 100644 index 0000000000000..068d0aafc7e52 --- /dev/null +++ b/homeassistant/components/mjpeg/util.py @@ -0,0 +1,18 @@ +"""Utilities for MJPEG IP Camera.""" + +import logging + + +class NoHeaderErrorFilter(logging.Filter): + """Filter out urllib3 Header Parsing Errors due to a urllib3 bug.""" + + def filter(self, record: logging.LogRecord) -> bool: + """Filter out Header Parsing Errors.""" + return "Failed to parse headers" not in record.getMessage() + + +def filter_urllib3_logging() -> None: + """Filter header errors from urllib3 due to a urllib3 bug.""" + urllib3_logger = logging.getLogger("urllib3.connectionpool") + if not any(isinstance(x, NoHeaderErrorFilter) for x in urllib3_logger.filters): + urllib3_logger.addFilter(NoHeaderErrorFilter()) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index d9d766974fdbe..4d40e42a47e23 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,4 +1,6 @@ """Binary sensor platform for mobile_app.""" +from typing import Any + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON @@ -18,7 +20,6 @@ ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_BINARY_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, - DATA_DEVICES, DOMAIN, ) from .entity import MobileAppEntity, unique_id @@ -39,7 +40,7 @@ async def async_setup_entry( for entry in entries: if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - config = { + config: dict[str, Any] = { ATTR_SENSOR_ATTRIBUTES: {}, ATTR_SENSOR_DEVICE_CLASS: entry.device_class or entry.original_device_class, ATTR_SENSOR_ICON: entry.original_icon, @@ -49,7 +50,7 @@ async def async_setup_entry( ATTR_SENSOR_UNIQUE_ID: entry.unique_id, ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } - entities.append(MobileAppBinarySensor(config, entry.device_id, config_entry)) + entities.append(MobileAppBinarySensor(config, config_entry)) async_add_entities(entities) @@ -65,9 +66,7 @@ def handle_sensor_registration(data): CONF_NAME ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" - device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] - - async_add_entities([MobileAppBinarySensor(data, device, config_entry)]) + async_add_entities([MobileAppBinarySensor(data, config_entry)]) async_dispatcher_connect( hass, diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index a2a4e15ee7272..ba81a0484cf04 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -28,6 +28,7 @@ ATTR_DEVICE_NAME = "device_name" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" +ATTR_NO_LEGACY_ENCRYPTION = "no_legacy_encryption" ATTR_OS_NAME = "os_name" ATTR_OS_VERSION = "os_version" ATTR_PUSH_WEBSOCKET_CHANNEL = "push_websocket_channel" diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index 3ad4309822541..d17702ec24fb7 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -7,6 +7,7 @@ from homeassistant.components.device_automation import InvalidDeviceAutomationConfig from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from .const import DOMAIN @@ -62,7 +63,7 @@ async def async_call_action_from_config( try: service_data[key] = template.render_complex(value_template, variables) - except template.TemplateError as err: + except TemplateError as err: raise InvalidDeviceAutomationConfig( f"Error rendering {key}: {err}" ) from err diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 67f3672ef8da1..5e2ae23af1655 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -37,7 +37,6 @@ async def async_setup_entry( """Set up OwnTracks based off an entry.""" entity = MobileAppEntity(entry) async_add_entities([entity]) - return True class MobileAppEntity(TrackerEntity, RestoreEntity): diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 0c26533b7cb50..0cb6cfc6fcc5d 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,4 +1,6 @@ """A entity class for mobile_app.""" +from __future__ import annotations + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ICON, @@ -8,7 +10,6 @@ STATE_UNAVAILABLE, ) from homeassistant.core import callback -from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity @@ -33,10 +34,9 @@ def unique_id(webhook_id, sensor_unique_id): class MobileAppEntity(RestoreEntity): """Representation of an mobile app entity.""" - def __init__(self, config: dict, device: DeviceEntry, entry: ConfigEntry) -> None: + def __init__(self, config: dict, entry: ConfigEntry) -> None: """Initialize the entity.""" self._config = config - self._device = device self._entry = entry self._registration = entry.data self._unique_id = config[CONF_UNIQUE_ID] diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index c4e0a81560b0f..545c3511fc939 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -7,7 +7,7 @@ import logging from aiohttp.web import Response, json_response -from nacl.encoding import Base64Encoder +from nacl.encoding import Base64Encoder, HexEncoder, RawEncoder from nacl.secret import SecretBox from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON @@ -23,6 +23,7 @@ ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, @@ -34,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_decrypt() -> tuple[int, Callable]: +def setup_decrypt(key_encoder) -> tuple[int, Callable]: """Return decryption function and length of key. Async friendly. @@ -42,12 +43,14 @@ def setup_decrypt() -> tuple[int, Callable]: def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).decrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, decrypt) -def setup_encrypt() -> tuple[int, Callable]: +def setup_encrypt(key_encoder) -> tuple[int, Callable]: """Return encryption function and length of key. Async friendly. @@ -55,15 +58,22 @@ def setup_encrypt() -> tuple[int, Callable]: def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" - return SecretBox(key).encrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).encrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, encrypt) -def _decrypt_payload(key: str, ciphertext: str) -> dict[str, str]: +def _decrypt_payload_helper( + key: str | None, + ciphertext: str, + get_key_bytes: Callable[[str, int], str | bytes], + key_encoder, +) -> dict[str, str] | None: """Decrypt encrypted payload.""" try: - keylen, decrypt = setup_decrypt() + keylen, decrypt = setup_decrypt(key_encoder) except OSError: _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") return None @@ -72,18 +82,33 @@ def _decrypt_payload(key: str, ciphertext: str) -> dict[str, str]: _LOGGER.warning("Ignoring encrypted payload because no decryption key known") return None - key = key.encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") + key_bytes = get_key_bytes(key, keylen) - try: - message = decrypt(ciphertext, key) - message = json.loads(message.decode("utf-8")) - _LOGGER.debug("Successfully decrypted mobile_app payload") - return message - except ValueError: - _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") - return None + msg_bytes = decrypt(ciphertext, key_bytes) + message = json.loads(msg_bytes.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + + +def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> str: + return key + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, HexEncoder) + + +def _decrypt_payload_legacy(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> bytes: + key_bytes = key.encode("utf-8") + key_bytes = key_bytes[:keylen] + key_bytes = key_bytes.ljust(keylen, b"\0") + return key_bytes + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, RawEncoder) def registration_context(registration: dict) -> Context: @@ -158,11 +183,16 @@ def webhook_response( data = json.dumps(data, cls=JSONEncoder) if registration[ATTR_SUPPORTS_ENCRYPTION]: - keylen, encrypt = setup_encrypt() - - key = registration[CONF_SECRET].encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") + keylen, encrypt = setup_encrypt( + HexEncoder if ATTR_NO_LEGACY_ENCRYPTION in registration else RawEncoder + ) + + if ATTR_NO_LEGACY_ENCRYPTION in registration: + key = registration[CONF_SECRET] + else: + key = registration[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b"\0") enc_data = encrypt(data.encode("utf-8"), key).decode("utf-8") data = json.dumps({"encrypted": True, "encrypted_data": enc_data}) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index a80e7db204ea7..ea8c56d1a7c42 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -6,7 +6,6 @@ import secrets from aiohttp.web import Request, Response -import emoji from nacl.secret import SecretBox import voluptuous as vol @@ -81,18 +80,8 @@ async def post(self, request: Request, data: dict) -> Response: data[CONF_USER_ID] = request["hass_user"].id - if slugify(data[ATTR_DEVICE_NAME], separator=""): - # if slug is not empty and would not only be underscores - # use DEVICE_NAME - pass - elif emoji.emoji_count(data[ATTR_DEVICE_NAME]): - # If otherwise empty string contains emoji - # use descriptive name of the first emoji - data[ATTR_DEVICE_NAME] = emoji.demojize( - emoji.emoji_lis(data[ATTR_DEVICE_NAME])[0]["emoji"] - ).replace(":", "") - else: - # Fallback to DEVICE_ID + # Fallback to DEVICE_ID if slug is empty. + if not slugify(data[ATTR_DEVICE_NAME], separator=""): data[ATTR_DEVICE_NAME] = data[ATTR_DEVICE_ID] await hass.async_create_task( diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 86adfbcfe054c..6cb4e964c9b5b 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,10 +3,11 @@ "name": "Mobile App", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": ["PyNaCl==1.4.0", "emoji==1.6.3"], + "requirements": ["PyNaCl==1.4.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["nacl"] } diff --git a/homeassistant/components/mobile_app/push_notification.py b/homeassistant/components/mobile_app/push_notification.py index f3852895d32ac..0ef9739c8bde3 100644 --- a/homeassistant/components/mobile_app/push_notification.py +++ b/homeassistant/components/mobile_app/push_notification.py @@ -28,7 +28,7 @@ def __init__( self.support_confirm = support_confirm self._send_message = send_message self.on_teardown = on_teardown - self.pending_confirms = {} + self.pending_confirms: dict[str, dict] = {} @callback def async_send_notification(self, data, fallback_send): diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 32477ccb50a7e..45bc4acd6a2d1 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,8 @@ """Sensor platform for mobile_app.""" from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -28,7 +30,6 @@ ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SENSOR_UOM, - DATA_DEVICES, DOMAIN, ) from .entity import MobileAppEntity, unique_id @@ -49,7 +50,7 @@ async def async_setup_entry( for entry in entries: if entry.domain != ENTITY_TYPE or entry.disabled_by: continue - config = { + config: dict[str, Any] = { ATTR_SENSOR_ATTRIBUTES: {}, ATTR_SENSOR_DEVICE_CLASS: entry.device_class or entry.original_device_class, ATTR_SENSOR_ICON: entry.original_icon, @@ -60,7 +61,7 @@ async def async_setup_entry( ATTR_SENSOR_UOM: entry.unit_of_measurement, ATTR_SENSOR_ENTITY_CATEGORY: entry.entity_category, } - entities.append(MobileAppSensor(config, entry.device_id, config_entry)) + entities.append(MobileAppSensor(config, config_entry)) async_add_entities(entities) @@ -76,9 +77,7 @@ def handle_sensor_registration(data): CONF_NAME ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" - device = hass.data[DOMAIN][DATA_DEVICES][data[CONF_WEBHOOK_ID]] - - async_add_entities([MobileAppSensor(data, device, config_entry)]) + async_add_entities([MobileAppSensor(data, config_entry)]) async_dispatcher_connect( hass, diff --git a/homeassistant/components/mobile_app/translations/cs.json b/homeassistant/components/mobile_app/translations/cs.json index 467536cc5ec68..2e39d1ab16c26 100644 --- a/homeassistant/components/mobile_app/translations/cs.json +++ b/homeassistant/components/mobile_app/translations/cs.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Odeslat ozn\u00e1men\u00ed" } - } + }, + "title": "Mobiln\u00ed aplikace" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/pt-BR.json b/homeassistant/components/mobile_app/translations/pt-BR.json index 4c211f4bc5392..f108db65cad9f 100644 --- a/homeassistant/components/mobile_app/translations/pt-BR.json +++ b/homeassistant/components/mobile_app/translations/pt-BR.json @@ -8,5 +8,11 @@ "description": "Deseja configurar o componente do aplicativo m\u00f3vel?" } } - } + }, + "device_automation": { + "action_type": { + "notify": "Enviar uma notifica\u00e7\u00e3o" + } + }, + "title": "Aplicativo mobile" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sk.json b/homeassistant/components/mobile_app/translations/sk.json new file mode 100644 index 0000000000000..3c056bf4f8540 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "install_app": "Pre nastavenie integr\u00e1cie s Home Assistant otvorte mobiln\u00fa aplik\u00e1ciu. Zoznam kompatibiln\u00fdch aplik\u00e1ci\u00ed n\u00e1jdete v [dokument\u00e1cii]({apps_url})." + }, + "step": { + "confirm": { + "description": "Chcete nastavi\u0165 komponentu Mobilnej aplik\u00e1cie?" + } + } + }, + "device_automation": { + "action_type": { + "notify": "Odosla\u0165 ozn\u00e1menie" + } + }, + "title": "Mobiln\u00e1 aplik\u00e1cia" +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index d659d7625c17c..860b8ef7b53db 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -7,6 +7,7 @@ import secrets from aiohttp.web import HTTPBadRequest, Request, Response, json_response +from nacl.exceptions import CryptoError from nacl.secret import SecretBox import voluptuous as vol @@ -58,6 +59,7 @@ ATTR_EVENT_TYPE, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, @@ -97,6 +99,7 @@ ) from .helpers import ( _decrypt_payload, + _decrypt_payload_legacy, empty_okay_response, error_response, registration_context, @@ -109,7 +112,7 @@ DELAY_SAVE = 10 -WEBHOOK_COMMANDS = Registry() +WEBHOOK_COMMANDS = Registry() # type: ignore[var-annotated] COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] @@ -191,7 +194,27 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + try: + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + data = {**config_entry.data, ATTR_NO_LEGACY_ENCRYPTION: True} + hass.config_entries.async_update_entry(config_entry, data=data) + except CryptoError: + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + try: + webhook_payload = _decrypt_payload_legacy( + config_entry.data[CONF_SECRET], enc_data + ) + except CryptoError: + _LOGGER.warning( + "Ignoring encrypted payload because unable to decrypt" + ) + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") + else: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error( diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 35a92dbb51b0a..0d609c87eb585 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": ["pymochad==0.2.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pbr", "pymochad"] } diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index cfe0aa370fec3..a5ad05a471195 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -74,6 +74,7 @@ CONF_RETRY_ON_EMPTY, CONF_REVERSE_ORDER, CONF_SCALE, + CONF_SLAVE_COUNT, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -118,7 +119,7 @@ { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.positive_int, - vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional(CONF_SLAVE, default=0): cv.positive_int, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.positive_int, @@ -139,9 +140,11 @@ vol.Optional(CONF_COUNT): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DataType.INT): vol.In( [ + DataType.INT8, DataType.INT16, DataType.INT32, DataType.INT64, + DataType.UINT8, DataType.UINT16, DataType.UINT32, DataType.UINT64, @@ -268,6 +271,7 @@ vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In( [CALL_TYPE_COIL, CALL_TYPE_DISCRETE] ), + vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, } ) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 615e86ae920e5..0727fa81e4430 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -312,7 +312,7 @@ async def async_update(self, now: datetime | None = None) -> None: self._lazy_errors = self._lazy_error_count self._attr_available = True - if self._verify_type == CALL_TYPE_COIL: + if self._verify_type in (CALL_TYPE_COIL, CALL_TYPE_DISCRETE): self._attr_is_on = bool(result.bits[0] & 1) else: value = int(result.registers[0]) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 07756b0f20781..50281bd2b2921 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,16 +2,31 @@ from __future__ import annotations from datetime import datetime +import logging +from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_DEVICE_CLASS, + CONF_NAME, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_hub from .base_platform import BasePlatform +from .const import CONF_SLAVE_COUNT +from .modbus import ModbusHub + +_LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 @@ -23,21 +38,51 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Modbus binary sensors.""" - sensors = [] if discovery_info is None: # pragma: no cover return + sensors: list[ModbusBinarySensor | SlaveSensor] = [] + hub = get_hub(hass, discovery_info[CONF_NAME]) for entry in discovery_info[CONF_BINARY_SENSORS]: - hub = get_hub(hass, discovery_info[CONF_NAME]) - sensors.append(ModbusBinarySensor(hub, entry)) - + slave_count = entry.get(CONF_SLAVE_COUNT, 0) + sensor = ModbusBinarySensor(hub, entry, slave_count) + if slave_count > 0: + sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry)) + sensors.append(sensor) async_add_entities(sensors) class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): """Modbus binary sensor.""" + def __init__(self, hub: ModbusHub, entry: dict[str, Any], slave_count: int) -> None: + """Initialize the Modbus binary sensor.""" + self._count = slave_count + 1 + self._coordinator: DataUpdateCoordinator[Any] | None = None + self._result = None + super().__init__(hub, entry) + + async def async_setup_slaves( + self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] + ) -> list[SlaveSensor]: + """Add slaves as needed (1 read for multiple sensors).""" + + # Add a dataCoordinator for each sensor that have slaves + # this ensures that idx = bit position of value in result + # polling is done with the base class + name = self._attr_name if self._attr_name else "modbus_sensor" + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + ) + + slaves: list[SlaveSensor] = [] + for idx in range(0, slave_count): + slaves.append(SlaveSensor(self._coordinator, idx, entry)) + return slaves + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -52,7 +97,7 @@ async def async_update(self, now: datetime | None = None) -> None: return self._call_active = True result = await self._hub.async_pymodbus_call( - self._slave, self._address, 1, self._input_type + self._slave, self._address, self._count, self._input_type ) self._call_active = False if result is None: @@ -61,10 +106,44 @@ async def async_update(self, now: datetime | None = None) -> None: return self._lazy_errors = self._lazy_error_count self._attr_available = False - self.async_write_ha_state() - return + self._result = None + else: + self._lazy_errors = self._lazy_error_count + self._attr_is_on = result.bits[0] & 1 + self._attr_available = True + self._result = result - self._lazy_errors = self._lazy_error_count - self._attr_is_on = result.bits[0] & 1 - self._attr_available = True self.async_write_ha_state() + if self._coordinator: + self._coordinator.async_set_updated_data(self._result) + + +class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): + """Modbus slave binary sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + ) -> None: + """Initialize the Modbus binary sensor.""" + idx += 1 + 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) + super().__init__(coordinator) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if state := await self.async_get_last_state(): + self._attr_is_on = state.state == STATE_ON + self.async_write_ha_state() + await super().async_added_to_hass() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + result = self.coordinator.data + if result: + self._attr_is_on = result.bits[self._result_inx] & self._result_bit + super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index dccd2eb49906b..934d14012f815 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -37,6 +37,7 @@ CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" CONF_SCALE = "scale" +CONF_SLAVE_COUNT = "slave_count" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OFF = "state_off" @@ -81,9 +82,11 @@ class DataType(str, Enum): INT = "int" # deprecated UINT = "uint" # deprecated STRING = "string" + INT8 = "int8" INT16 = "int16" INT32 = "int32" INT64 = "int64" + UINT8 = "uint8" UINT16 = "uint16" UINT32 = "uint32" UINT64 = "uint64" diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index ccf2bf8138496..96127f39bbd56 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pymodbus==2.5.3"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "gold", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymodbus"] } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 0ea4c57d4d955..20083bb3d1cef 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -358,7 +358,7 @@ def _pymodbus_connect(self) -> bool: return True def _pymodbus_call( - self, unit: int, address: int, value: int | list[int], use_call: str + self, unit: int | None, address: int, value: int | list[int], use_call: str ) -> ModbusResponse: """Call sync. pymodbus.""" kwargs = {"unit": unit} if unit else {} diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index ca0f370b56269..347e8d3fc7284 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -58,9 +58,11 @@ } ENTRY = namedtuple("ENTRY", ["struct_id", "register_count"]) DEFAULT_STRUCT_FORMAT = { + DataType.INT8: ENTRY("b", 1), DataType.INT16: ENTRY("h", 1), DataType.INT32: ENTRY("i", 2), DataType.INT64: ENTRY("q", 4), + DataType.UINT8: ENTRY("c", 1), DataType.UINT16: ENTRY("H", 1), DataType.UINT32: ENTRY("I", 2), DataType.UINT64: ENTRY("Q", 4), @@ -207,8 +209,7 @@ def duplicate_entity_validator(config: dict) -> dict: addr += "_" + str(entry[CONF_COMMAND_ON]) if CONF_COMMAND_OFF in entry: addr += "_" + str(entry[CONF_COMMAND_OFF]) - if CONF_SLAVE in entry: - addr += "_" + str(entry[CONF_SLAVE]) + addr += "_" + str(entry.get(CONF_SLAVE, 0)) if addr in addresses: err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" _LOGGER.warning(err) diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index d66be29f8b72e..8f62cf4beb5ab 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -8,7 +8,7 @@ from .const import DATA_KEY_API, DOMAIN, EXCEPTIONS -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/modem_callerid/button.py b/homeassistant/components/modem_callerid/button.py new file mode 100644 index 0000000000000..63a88a8a4e5f6 --- /dev/null +++ b/homeassistant/components/modem_callerid/button.py @@ -0,0 +1,47 @@ +"""Support for Phone Modem button.""" +from __future__ import annotations + +from phone_modem import PhoneModem + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform + +from .const import DATA_KEY_API, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up the Modem Caller ID sensor.""" + api = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + async_add_entities( + [ + PhoneModemButton( + api, + entry.data[CONF_DEVICE], + entry.entry_id, + ) + ] + ) + + +class PhoneModemButton(ButtonEntity): + """Implementation of USB modem caller ID button.""" + + _attr_icon = "mdi:phone-hangup" + _attr_name = "Phone Modem Reject" + + def __init__(self, api: PhoneModem, device: str, server_unique_id: str) -> None: + """Initialize the button.""" + self.device = device + self.api = api + self._attr_unique_id = server_unique_id + + async def async_press(self) -> None: + """Press the button.""" + await self.api.reject_call(self.device) diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 4f4264d768837..ae66e72bfcb2c 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@tkdrob"], "dependencies": ["usb"], "iot_class": "local_polling", - "usb": [{"vid":"0572","pid":"1340"}] + "usb": [{"vid":"0572","pid":"1340"}], + "loggers": ["phone_modem"] } diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 94c7c51ee8051..f4b2f3c3e44fe 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,6 +1,8 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" from __future__ import annotations +import logging + from phone_modem import PhoneModem from homeassistant.components.sensor import SensorEntity @@ -11,6 +13,8 @@ from .const import CID, DATA_KEY_API, DOMAIN, ICON, SERVICE_REJECT_CALL +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -40,7 +44,6 @@ async def _async_on_hass_stop(event: Event) -> None: ) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_REJECT_CALL, {}, "async_reject_call") @@ -85,4 +88,9 @@ def _async_incoming_call(self, new_state: str) -> None: async def async_reject_call(self) -> None: """Reject Incoming Call.""" + _LOGGER.warning( + "Calling reject_call service is deprecated and will be removed after 2022.4; " + "A new button entity is now available with the same function " + "and replaces the existing service" + ) await self.api.reject_call(self.device) diff --git a/homeassistant/components/modem_callerid/translations/el.json b/homeassistant/components/modem_callerid/translations/el.json new file mode 100644 index 0000000000000..6d3961cc258d5 --- /dev/null +++ b/homeassistant/components/modem_callerid/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", + "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 \u03ac\u03bb\u03bb\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "usb_confirm": { + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2.", + "title": "\u039c\u03cc\u03bd\u03c4\u03b5\u03bc \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5" + }, + "user": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2.", + "title": "\u039c\u03cc\u03bd\u03c4\u03b5\u03bc \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/pt-BR.json b/homeassistant/components/modem_callerid/translations/pt-BR.json new file mode 100644 index 0000000000000..39394d0752e2a --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "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 restante encontrado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "usb_confirm": { + "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida.", + "title": "Modem do telefone" + }, + "user": { + "data": { + "name": "Nome", + "port": "Porta" + }, + "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida.", + "title": "Modem do telefone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/sk.json b/homeassistant/components/modem_callerid/translations/sk.json new file mode 100644 index 0000000000000..f7ef4cd289d5f --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/manifest.json b/homeassistant/components/modern_forms/manifest.json index 1466537259b1b..67a7581e8973e 100644 --- a/homeassistant/components/modern_forms/manifest.json +++ b/homeassistant/components/modern_forms/manifest.json @@ -12,5 +12,6 @@ "codeowners": [ "@wonderslug" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiomodernforms"] } diff --git a/homeassistant/components/modern_forms/translations/el.json b/homeassistant/components/modern_forms/translations/el.json index 5cdd0da50b555..b8cb77ffa9979 100644 --- a/homeassistant/components/modern_forms/translations/el.json +++ b/homeassistant/components/modern_forms/translations/el.json @@ -1,7 +1,23 @@ { "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": { + "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;" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1 Modern Forms \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." + }, "zeroconf_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1 Modern Forms \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 `{name}` \u03c3\u03c4\u03bf Home Assistant;", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03c9\u03bd Modern Forms" diff --git a/homeassistant/components/modern_forms/translations/pt-BR.json b/homeassistant/components/modern_forms/translations/pt-BR.json new file mode 100644 index 0000000000000..07a68e2fb84de --- /dev/null +++ b/homeassistant/components/modern_forms/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "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": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure seu ventilador do Modern Forms para integrar com o Home Assistant." + }, + "zeroconf_confirm": { + "description": "Deseja adicionar o f\u00e3 do Modern Forms chamado `{name}` ao Home Assistant?", + "title": "Dispositivo de ventilador do Modern Forms descoberto" + } + } + }, + "title": "Formas modernas" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py new file mode 100644 index 0000000000000..62e18917dc603 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -0,0 +1,159 @@ +"""Support for the Moehlenhoff Alpha2.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import aiohttp +from moehlenhoff_alpha2 import Alpha2Base + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.CLIMATE] + +UPDATE_INTERVAL = timedelta(seconds=60) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + base = Alpha2Base(entry.data["host"]) + coordinator = Alpha2BaseCoordinator(hass, base) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(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 and entry.entry_id in hass.data[DOMAIN]: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): + """Keep the base instance in one place and centralize the update.""" + + def __init__(self, hass: HomeAssistant, base: Alpha2Base) -> None: + """Initialize Alpha2Base data updater.""" + self.base = base + super().__init__( + hass=hass, + logger=_LOGGER, + name="alpha2_base", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, dict]: + """Fetch the latest data from the source.""" + await self.base.update_data() + return {ha["ID"]: ha for ha in self.base.heat_areas if ha.get("ID")} + + def get_cooling(self) -> bool: + """Return if cooling mode is enabled.""" + return self.base.cooling + + async def async_set_cooling(self, enabled: bool) -> None: + """Enable or disable cooling mode.""" + await self.base.set_cooling(enabled) + for update_callback in self._listeners: + update_callback() + + async def async_set_target_temperature( + self, heat_area_id: str, target_temperature: float + ) -> None: + """Set the target temperature of the given heat area.""" + _LOGGER.debug( + "Setting target temperature of heat area %s to %0.1f", + heat_area_id, + target_temperature, + ) + + update_data = {"T_TARGET": target_temperature} + is_cooling = self.get_cooling() + heat_area_mode = self.data[heat_area_id]["HEATAREA_MODE"] + if heat_area_mode == 1: + if is_cooling: + update_data["T_COOL_DAY"] = target_temperature + else: + update_data["T_HEAT_DAY"] = target_temperature + elif heat_area_mode == 2: + if is_cooling: + update_data["T_COOL_NIGHT"] = target_temperature + else: + update_data["T_HEAT_NIGHT"] = target_temperature + + try: + await self.base.update_heat_area(heat_area_id, update_data) + except aiohttp.ClientError as http_err: + raise HomeAssistantError( + "Failed to set target temperature, communication error with alpha2 base" + ) from http_err + self.data[heat_area_id].update(update_data) + for update_callback in self._listeners: + update_callback() + + async def async_set_heat_area_mode( + self, heat_area_id: str, heat_area_mode: int + ) -> None: + """Set the mode of the given heat area.""" + # HEATAREA_MODE: 0=Auto, 1=Tag, 2=Nacht + if heat_area_mode not in (0, 1, 2): + ValueError(f"Invalid heat area mode: {heat_area_mode}") + _LOGGER.debug( + "Setting mode of heat area %s to %d", + heat_area_id, + heat_area_mode, + ) + try: + await self.base.update_heat_area( + heat_area_id, {"HEATAREA_MODE": heat_area_mode} + ) + except aiohttp.ClientError as http_err: + raise HomeAssistantError( + "Failed to set heat area mode, communication error with alpha2 base" + ) from http_err + + self.data[heat_area_id]["HEATAREA_MODE"] = heat_area_mode + is_cooling = self.get_cooling() + if heat_area_mode == 1: + if is_cooling: + self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ + "T_COOL_DAY" + ] + else: + self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ + "T_HEAT_DAY" + ] + elif heat_area_mode == 2: + if is_cooling: + self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ + "T_COOL_NIGHT" + ] + else: + self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ + "T_HEAT_NIGHT" + ] + for update_callback in self._listeners: + update_callback() diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py new file mode 100644 index 0000000000000..d99eb0e4c8c6a --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -0,0 +1,130 @@ +"""Support for Alpha2 room control unit via Alpha2 base.""" +import logging + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import Alpha2BaseCoordinator +from .const import DOMAIN, PRESET_AUTO, PRESET_DAY, PRESET_NIGHT + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Alpha2Climate entities from a config_entry.""" + + coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Alpha2Climate(coordinator, heat_area_id) for heat_area_id in coordinator.data + ) + + +# https://developers.home-assistant.io/docs/core/entity/climate/ +class Alpha2Climate(CoordinatorEntity, ClimateEntity): + """Alpha2 ClimateEntity.""" + + coordinator: Alpha2BaseCoordinator + target_temperature_step = 0.2 + + _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + _attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_COOL] + _attr_temperature_unit = TEMP_CELSIUS + _attr_preset_modes = [PRESET_AUTO, PRESET_DAY, PRESET_NIGHT] + + def __init__(self, coordinator: Alpha2BaseCoordinator, heat_area_id: str) -> None: + """Initialize Alpha2 ClimateEntity.""" + super().__init__(coordinator) + self.heat_area_id = heat_area_id + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self.coordinator.data[self.heat_area_id]["HEATAREA_NAME"] + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return float(self.coordinator.data[self.heat_area_id].get("T_TARGET_MIN", 0.0)) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return float(self.coordinator.data[self.heat_area_id].get("T_TARGET_MAX", 30.0)) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return float(self.coordinator.data[self.heat_area_id].get("T_ACTUAL", 0.0)) + + @property + def hvac_mode(self) -> str: + """Return current hvac mode.""" + if self.coordinator.get_cooling(): + return HVAC_MODE_COOL + return HVAC_MODE_HEAT + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.coordinator.async_set_cooling(hvac_mode == HVAC_MODE_COOL) + + @property + def hvac_action(self) -> str: + """Return the current running hvac operation.""" + if not self.coordinator.data[self.heat_area_id]["_HEATCTRL_STATE"]: + return CURRENT_HVAC_IDLE + if self.coordinator.get_cooling(): + return CURRENT_HVAC_COOL + return CURRENT_HVAC_HEAT + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return float(self.coordinator.data[self.heat_area_id].get("T_TARGET", 0.0)) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperatures.""" + if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + + await self.coordinator.async_set_target_temperature( + self.heat_area_id, target_temperature + ) + + @property + def preset_mode(self) -> str: + """Return the current preset mode.""" + if self.coordinator.data[self.heat_area_id]["HEATAREA_MODE"] == 1: + return PRESET_DAY + if self.coordinator.data[self.heat_area_id]["HEATAREA_MODE"] == 2: + return PRESET_NIGHT + return PRESET_AUTO + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new operation mode.""" + heat_area_mode = 0 + if preset_mode == PRESET_DAY: + heat_area_mode = 1 + elif preset_mode == PRESET_NIGHT: + heat_area_mode = 2 + + await self.coordinator.async_set_heat_area_mode( + self.heat_area_id, heat_area_mode + ) diff --git a/homeassistant/components/moehlenhoff_alpha2/config_flow.py b/homeassistant/components/moehlenhoff_alpha2/config_flow.py new file mode 100644 index 0000000000000..cafdca040b369 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/config_flow.py @@ -0,0 +1,55 @@ +"""Alpha2 config flow.""" +import asyncio +import logging + +import aiohttp +from moehlenhoff_alpha2 import Alpha2Base +import voluptuous as vol + +from homeassistant import config_entries + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required("host"): str}) + + +async def validate_input(data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + base = Alpha2Base(data["host"]) + try: + await base.update_data() + except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): + return {"error": "cannot_connect"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return {"error": "unknown"} + + # Return info that you want to store in the config entry. + return {"title": base.name} + + +class Alpha2BaseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Möhlenhoff Alpha2 config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + self._async_abort_entries_match({"host": user_input["host"]}) + result = await validate_input(user_input) + if result.get("error"): + errors["base"] = result["error"] + else: + return self.async_create_entry(title=result["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/moehlenhoff_alpha2/const.py b/homeassistant/components/moehlenhoff_alpha2/const.py new file mode 100644 index 0000000000000..268936982bd89 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/const.py @@ -0,0 +1,6 @@ +"""Constants for the Alpha2 integration.""" + +DOMAIN = "moehlenhoff_alpha2" +PRESET_AUTO = "auto" +PRESET_DAY = "day" +PRESET_NIGHT = "night" diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json new file mode 100644 index 0000000000000..b755b28f826ce --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "moehlenhoff_alpha2", + "name": "Möhlenhoff Alpha 2", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", + "requirements": ["moehlenhoff-alpha2==1.1.2"], + "iot_class": "local_push", + "codeowners": [ + "@j-a-n" + ] +} diff --git a/homeassistant/components/moehlenhoff_alpha2/strings.json b/homeassistant/components/moehlenhoff_alpha2/strings.json new file mode 100644 index 0000000000000..3347b2f318c14 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/strings.json @@ -0,0 +1,19 @@ +{ + "title": "Möhlenhoff Alpha2", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "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/moehlenhoff_alpha2/translations/bg.json b/homeassistant/components/moehlenhoff_alpha2/translations/bg.json new file mode 100644 index 0000000000000..48e3fdefc608f --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/ca.json b/homeassistant/components/moehlenhoff_alpha2/translations/ca.json new file mode 100644 index 0000000000000..120b7ef6d4aa8 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/cs.json b/homeassistant/components/moehlenhoff_alpha2/translations/cs.json new file mode 100644 index 0000000000000..5eac883adf06f --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/cs.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/de.json b/homeassistant/components/moehlenhoff_alpha2/translations/de.json new file mode 100644 index 0000000000000..b35bb0c25cc16 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/el.json b/homeassistant/components/moehlenhoff_alpha2/translations/el.json new file mode 100644 index 0000000000000..d1e9ebe7f198a --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/el.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/en.json b/homeassistant/components/moehlenhoff_alpha2/translations/en.json new file mode 100644 index 0000000000000..d2bae9be52cff --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/es.json b/homeassistant/components/moehlenhoff_alpha2/translations/es.json new file mode 100644 index 0000000000000..81e3d96b7bc52 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Anfitri\u00f3n" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/et.json b/homeassistant/components/moehlenhoff_alpha2/translations/et.json new file mode 100644 index 0000000000000..8f3df1c4b71dc --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/fr.json b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json new file mode 100644 index 0000000000000..205436aa03d11 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/he.json b/homeassistant/components/moehlenhoff_alpha2/translations/he.json new file mode 100644 index 0000000000000..1699e0f8e1983 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/he.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/hu.json b/homeassistant/components/moehlenhoff_alpha2/translations/hu.json new file mode 100644 index 0000000000000..dfe114ef06869 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/hu.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/id.json b/homeassistant/components/moehlenhoff_alpha2/translations/id.json new file mode 100644 index 0000000000000..1a0e5f47ccfa2 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/it.json b/homeassistant/components/moehlenhoff_alpha2/translations/it.json new file mode 100644 index 0000000000000..72abc5f1e5f95 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/ja.json b/homeassistant/components/moehlenhoff_alpha2/translations/ja.json new file mode 100644 index 0000000000000..7de2c8da4d885 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/ja.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/lv.json b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json new file mode 100644 index 0000000000000..d15111e97b0d1 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json @@ -0,0 +1,3 @@ +{ + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/nl.json b/homeassistant/components/moehlenhoff_alpha2/translations/nl.json new file mode 100644 index 0000000000000..0ac34b3153fb4 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/no.json b/homeassistant/components/moehlenhoff_alpha2/translations/no.json new file mode 100644 index 0000000000000..32fae944a717d --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/pl.json b/homeassistant/components/moehlenhoff_alpha2/translations/pl.json new file mode 100644 index 0000000000000..6fa4bed7b54c1 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/pl.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/pt-BR.json b/homeassistant/components/moehlenhoff_alpha2/translations/pt-BR.json new file mode 100644 index 0000000000000..d322f29155397 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/ru.json b/homeassistant/components/moehlenhoff_alpha2/translations/ru.json new file mode 100644 index 0000000000000..e843c048d8917 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/tr.json b/homeassistant/components/moehlenhoff_alpha2/translations/tr.json new file mode 100644 index 0000000000000..ff0498a01a114 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/zh-Hant.json b/homeassistant/components/moehlenhoff_alpha2/translations/zh-Hant.json new file mode 100644 index 0000000000000..2105be9fc8930 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index fe8a7c2431d30..bbd609f5fb8a3 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -205,9 +205,8 @@ def _update_temp_sensor(state): return None unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - temp = util.convert(state.state, float) - if temp is None: + if (temp := util.convert(state.state, float)) is None: _LOGGER.error( "Unable to parse temperature sensor %s with state: %s", state.entity_id, diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index e018ef94f7d98..91fd353f2e0ae 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -63,6 +63,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +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/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 2001531a396eb..85910b0eb9ad0 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pymonoprice==0.3"], "codeowners": ["@etsinko", "@OnFreund"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymonoprice"] } diff --git a/homeassistant/components/monoprice/translations/el.json b/homeassistant/components/monoprice/translations/el.json new file mode 100644 index 0000000000000..d1a0acdec2643 --- /dev/null +++ b/homeassistant/components/monoprice/translations/el.json @@ -0,0 +1,40 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "port": "\u0398\u03cd\u03c1\u03b1", + "source_1": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #1", + "source_2": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #2", + "source_3": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #3", + "source_4": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #4", + "source_5": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #5", + "source_6": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #6" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #1", + "source_2": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #2", + "source_3": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #3", + "source_4": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #4", + "source_5": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #5", + "source_6": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #6" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b7\u03b3\u03ce\u03bd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/pt-BR.json b/homeassistant/components/monoprice/translations/pt-BR.json index 4eb010468f392..2935c6510be22 100644 --- a/homeassistant/components/monoprice/translations/pt-BR.json +++ b/homeassistant/components/monoprice/translations/pt-BR.json @@ -1,12 +1,16 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "port": "Porta", "source_1": "Nome da fonte #1", "source_2": "Nome da fonte #2", "source_3": "Nome da fonte #3", @@ -17,5 +21,20 @@ "title": "Conecte-se ao dispositivo" } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome da fonte #1", + "source_2": "Nome da fonte #2", + "source_3": "Nome da fonte #3", + "source_4": "Nome da fonte #4", + "source_5": "Nome da fonte #5", + "source_6": "Nome da fonte #6" + }, + "title": "Configurar as fontes" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/sk.json b/homeassistant/components/monoprice/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/monoprice/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 97bc7ec4673ba..cd10d9168d9c6 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -4,7 +4,10 @@ from astral import moon import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -34,7 +37,7 @@ STATE_WAXING_GIBBOUS: "mdi:moon-waxing-gibbous", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} ) @@ -46,7 +49,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" - name = config.get(CONF_NAME) + name: str = config[CONF_NAME] async_add_entities([MoonSensor(name)], True) @@ -54,46 +57,32 @@ async def async_setup_platform( class MoonSensor(SensorEntity): """Representation of a Moon sensor.""" - def __init__(self, name): + _attr_device_class = "moon__phase" + + def __init__(self, name: str) -> None: """Initialize the moon sensor.""" - self._name = name - self._state = None - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def device_class(self): - """Return the device class of the entity.""" - return "moon__phase" - - @property - def native_value(self): - """Return the state of the device.""" - if self._state < 0.5 or self._state > 27.5: - return STATE_NEW_MOON - if self._state < 6.5: - return STATE_WAXING_CRESCENT - if self._state < 7.5: - return STATE_FIRST_QUARTER - if self._state < 13.5: - return STATE_WAXING_GIBBOUS - if self._state < 14.5: - return STATE_FULL_MOON - if self._state < 20.5: - return STATE_WANING_GIBBOUS - if self._state < 21.5: - return STATE_LAST_QUARTER - return STATE_WANING_CRESCENT - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return MOON_ICONS.get(self.state) + self._attr_name = name async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = moon.phase(today) + state = moon.phase(today) + + if state < 0.5 or state > 27.5: + self._attr_native_value = STATE_NEW_MOON + elif state < 6.5: + self._attr_native_value = STATE_WAXING_CRESCENT + elif state < 7.5: + self._attr_native_value = STATE_FIRST_QUARTER + elif state < 13.5: + self._attr_native_value = STATE_WAXING_GIBBOUS + elif state < 14.5: + self._attr_native_value = STATE_FULL_MOON + elif state < 20.5: + self._attr_native_value = STATE_WANING_GIBBOUS + elif state < 21.5: + self._attr_native_value = STATE_LAST_QUARTER + else: + self._attr_native_value = STATE_WANING_CRESCENT + + self._attr_icon = MOON_ICONS.get(self._attr_native_value) diff --git a/homeassistant/components/moon/translations/sensor.el.json b/homeassistant/components/moon/translations/sensor.el.json new file mode 100644 index 0000000000000..59b00e329afb3 --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.el.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03c4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf", + "full_moon": "\u03a0\u03b1\u03bd\u03c3\u03ad\u03bb\u03b7\u03bd\u03bf\u03c2", + "last_quarter": "\u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf \u03c4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf", + "new_moon": "\u039d\u03ad\u03b1 \u03a3\u03b5\u03bb\u03ae\u03bd\u03b7", + "waning_crescent": "\u03a6\u03b8\u03af\u03bd\u03c9\u03bd \u039c\u03b7\u03bd\u03af\u03c3\u03ba\u03bf\u03c2", + "waning_gibbous": "\u03a6\u03b8\u03af\u03bd\u03c9\u03bd \u0391\u03bc\u03c6\u03af\u03ba\u03c5\u03c1\u03c4\u03bf\u03c2", + "waxing_crescent": "\u0391\u03cd\u03be\u03c9\u03bd \u039c\u03b7\u03bd\u03af\u03c3\u03ba\u03bf\u03c2", + "waxing_gibbous": "\u0391\u03cd\u03be\u03c9\u03bd \u0391\u03bc\u03c6\u03af\u03ba\u03c5\u03c1\u03c4\u03bf\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index f5eff45539e59..9bc952a21eaf3 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -51,6 +51,7 @@ BlindType.ShangriLaBlind: CoverDeviceClass.BLIND, BlindType.DoubleRoller: CoverDeviceClass.SHADE, BlindType.VerticalBlind: CoverDeviceClass.BLIND, + BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND, } TDBU_DEVICE_MAP = { diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index fc664910c843a..c904320d9af77 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,8 +3,9 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.12"], + "requirements": ["motionblinds==0.5.13"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["motionblinds"] } diff --git a/homeassistant/components/motion_blinds/translations/el.json b/homeassistant/components/motion_blinds/translations/el.json new file mode 100644 index 0000000000000..2914382e82099 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/el.json @@ -0,0 +1,50 @@ +{ + "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", + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "discovery_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2", + "invalid_interface": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "flow_title": "Motion Blinds", + "step": { + "connect": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "interface": "\u0397 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" + }, + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API 16 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2.", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "description": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03c0\u03cd\u03bb\u03b5\u03c2 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2.", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Motion Gateway, \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", + "title": "Motion Blinds" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "\u0391\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 multicast push \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ce\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json index 0d9e257feba90..dcabdbd16e5c7 100644 --- a/homeassistant/components/motion_blinds/translations/pt-BR.json +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -1,25 +1,49 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "connection_error": "Falha ao conectar" }, + "error": { + "discovery_error": "Falha ao descobrir um Motion Gateway", + "invalid_interface": "Interface de rede inv\u00e1lida" + }, + "flow_title": "Cortinas de movimento", "step": { "connect": { "data": { + "api_key": "Chave da API", "interface": "A interface de rede a ser utilizada" - } + }, + "description": "Voc\u00ea precisar\u00e1 da chave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obter instru\u00e7\u00f5es", + "title": "Cortinas de movimento" }, "select": { "data": { - "select_ip": "Endere\u00e7o de IP" - } + "select_ip": "Endere\u00e7o IP" + }, + "description": "Execute a configura\u00e7\u00e3o novamente se desejar conectar Motion Gateways adicionais", + "title": "Selecione o Motion Gateway que voc\u00ea deseja conectar" + }, + "user": { + "data": { + "api_key": "Chave da API", + "host": "Endere\u00e7o IP" + }, + "description": "Conecte-se ao seu Motion Gateway, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada", + "title": "Cortinas de movimento" } } }, "options": { "step": { "init": { - "description": "Especifique as configura\u00e7\u00f5es opcionais" + "data": { + "wait_for_push": "Aguarde o push multicast na atualiza\u00e7\u00e3o" + }, + "description": "Especifique as configura\u00e7\u00f5es opcionais", + "title": "Cortinas de movimento" } } } diff --git a/homeassistant/components/motion_blinds/translations/sk.json b/homeassistant/components/motion_blinds/translations/sk.json new file mode 100644 index 0000000000000..e58538162d7ac --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "connect": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index b9b7f6d42c8de..e5e4f224fe6da 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -24,10 +24,9 @@ ) import voluptuous as vol -from homeassistant.components.mjpeg.camera import ( +from homeassistant.components.mjpeg import ( CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, - CONF_VERIFY_SSL, MjpegCamera, ) from homeassistant.config_entries import ConfigEntry @@ -144,6 +143,8 @@ def camera_add(camera: dict[str, Any]) -> None: class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): """motionEye mjpeg camera.""" + _name: str + def __init__( self, config_entry_id: str, @@ -173,10 +174,8 @@ def __init__( ) MjpegCamera.__init__( self, - { - CONF_VERIFY_SSL: False, - **self._get_mjpeg_camera_properties_for_camera(camera), - }, + verify_ssl=False, + **self._get_mjpeg_camera_properties_for_camera(camera), ) @callback @@ -207,7 +206,7 @@ def _get_mjpeg_camera_properties_for_camera( return { CONF_NAME: camera[KEY_NAME], CONF_USERNAME: self._surveillance_username if auth is not None else None, - CONF_PASSWORD: self._surveillance_password if auth is not None else None, + CONF_PASSWORD: self._surveillance_password if auth is not None else "", CONF_MJPEG_URL: streaming_url or "", CONF_STILL_IMAGE_URL: self._client.get_camera_snapshot_url(camera), CONF_AUTHENTICATION: auth, @@ -227,7 +226,10 @@ def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None: self._still_image_url = properties[CONF_STILL_IMAGE_URL] self._authentication = properties[CONF_AUTHENTICATION] - if self._authentication == HTTP_BASIC_AUTHENTICATION: + if ( + self._authentication == HTTP_BASIC_AUTHENTICATION + and self._username is not None + ): self._auth = aiohttp.BasicAuth(self._username, password=self._password) def _is_acceptable_streaming_camera(self) -> bool: diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index e01cae085110d..5c1dbb376a03c 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -3,16 +3,10 @@ "name": "motionEye", "documentation": "https://www.home-assistant.io/integrations/motioneye", "config_flow": true, - "dependencies": [ - "http", - "media_source", - "webhook" - ], - "requirements": [ - "motioneye-client==0.3.12" - ], - "codeowners": [ - "@dermotduffy" - ], - "iot_class": "local_polling" + "dependencies": ["http", "webhook"], + "after_dependencies": ["media_source"], + "requirements": ["motioneye-client==0.3.12"], + "codeowners": ["@dermotduffy"], + "iot_class": "local_polling", + "loggers": ["motioneye_client"] } diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 8c7b86ca17321..915cc30897ec2 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -134,8 +134,7 @@ def _get_config_or_raise(self, config_id: str) -> ConfigEntry: def _get_device_or_raise(self, device_id: str) -> dr.DeviceEntry: """Get a config entry from a URL.""" device_registry = dr.async_get(self.hass) - device = device_registry.async_get(device_id) - if not device: + if not (device := device_registry.async_get(device_id)): raise MediaSourceError(f"Unable to find device with id: {device_id}") return device diff --git a/homeassistant/components/motioneye/translations/el.json b/homeassistant/components/motioneye/translations/el.json index b0c39d2c59776..d65df6be671fa 100644 --- a/homeassistant/components/motioneye/translations/el.json +++ b/homeassistant/components/motioneye/translations/el.json @@ -1,14 +1,38 @@ { "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", + "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_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + "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_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "hassio_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 motionEye \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf: {addon};", + "title": "motionEye \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Home Assistant" + }, + "user": { + "data": { + "admin_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae", + "admin_username": "\u0394\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2 \u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "surveillance_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7\u03c2", + "surveillance_username": "\u0395\u03c0\u03b9\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + } + } } }, "options": { "step": { "init": { "data": { - "stream_url_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf URL \u03c1\u03bf\u03ae\u03c2" + "stream_url_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf URL \u03c1\u03bf\u03ae\u03c2", + "webhook_set": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 webhooks motionEye \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant", + "webhook_set_overwrite": "\u0391\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03c9\u03bd webhooks" } } } diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json index 4bc75878b2b2e..f8b99b0c09b09 100644 --- a/homeassistant/components/motioneye/translations/it.json +++ b/homeassistant/components/motioneye/translations/it.json @@ -17,9 +17,9 @@ }, "user": { "data": { - "admin_password": "Amministratore Password", + "admin_password": "Password di amministrazione", "admin_username": "Amministratore Nome utente", - "surveillance_password": "Sorveglianza Password", + "surveillance_password": "Password di sorveglianza", "surveillance_username": "Sorveglianza Nome utente", "url": "URL" } diff --git a/homeassistant/components/motioneye/translations/pt-BR.json b/homeassistant/components/motioneye/translations/pt-BR.json index ec20df0207433..2ede694dcbaef 100644 --- a/homeassistant/components/motioneye/translations/pt-BR.json +++ b/homeassistant/components/motioneye/translations/pt-BR.json @@ -1,19 +1,38 @@ { "config": { "abort": { - "already_configured": "Servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_url": "URL inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao servi\u00e7o motionEye fornecido pelo add-on: {addon} ?", + "title": "motionEye via Home Assistant add-on" + }, + "user": { + "data": { + "admin_password": "Senha Administrador", + "admin_username": "Usu\u00e1rio", + "surveillance_password": "Senha Vigil\u00e2ncia", + "surveillance_username": "Usu\u00e1rio", + "url": "URL" + } + } } }, "options": { "step": { "init": { "data": { - "stream_url_template": "Modelo de URL de fluxo" + "stream_url_template": "Modelo de URL de fluxo", + "webhook_set": "Configure os webhooks do motionEye para relatar eventos ao Home Assistant", + "webhook_set_overwrite": "Substituir webhooks n\u00e3o reconhecidos" } } } diff --git a/homeassistant/components/motioneye/translations/sk.json b/homeassistant/components/motioneye/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/motioneye/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index 39b4e45196b76..880d32b587795 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": ["python-mpd2==3.0.4"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["mpd"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index d2c6b7eeaf335..4d3df8f00ca21 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -119,6 +119,9 @@ def __init__(self, server, port, password, name): self._muted_volume = None self._media_position_updated_at = None self._media_position = None + self._media_image_hash = None + # Track if the song changed so image doesn't have to be loaded every update. + self._media_image_file = None self._commands = None # set up MPD client @@ -149,6 +152,7 @@ async def _fetch_status(self): """Fetch status from MPD.""" self._status = await self._client.status() self._currentsong = await self._client.currentsong() + await self._async_update_media_image_hash() if (position := self._status.get("elapsed")) is None: position = self._status.get("time") @@ -265,16 +269,46 @@ def media_album_name(self): @property def media_image_hash(self): """Hash value for media image.""" - if file := self._currentsong.get("file"): - return hashlib.sha256(file.encode("utf-8")).hexdigest()[:16] - - return None + return self._media_image_hash async def async_get_media_image(self): """Fetch media image of current playing track.""" if not (file := self._currentsong.get("file")): return None, None + response = await self._async_get_file_image_response(file) + if response is None: + return None, None + + image = bytes(response["binary"]) + mime = response.get( + "type", "image/png" + ) # readpicture has type, albumart does not + return (image, mime) + async def _async_update_media_image_hash(self): + """Update the hash value for the media image.""" + file = self._currentsong.get("file") + + if file == self._media_image_file: + return + + if ( + file is not None + and (response := await self._async_get_file_image_response(file)) + is not None + ): + self._media_image_hash = hashlib.sha256( + bytes(response["binary"]) + ).hexdigest()[:16] + else: + # If there is no image, this hash has to be None, else the media player component + # assumes there is an image and returns an error trying to load it and the + # frontend media control card breaks. + self._media_image_hash = None + + self._media_image_file = file + + async def _async_get_file_image_response(self, file): # not all MPD implementations and versions support the `albumart` and `fetchpicture` commands can_albumart = "albumart" in self._commands can_readpicture = "readpicture" in self._commands @@ -303,14 +337,11 @@ async def async_get_media_image(self): error, ) + # response can be an empty object if there is no image if not response: - return None, None + return None - image = bytes(response.get("binary")) - mime = response.get( - "type", "image/png" - ) # readpicture has type, albumart does not - return (image, mime) + return response @property def volume_level(self): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 5663f34329697..107bc4660c2a1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -37,6 +37,7 @@ CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, Platform, ) from homeassistant.core import ( @@ -49,7 +50,13 @@ ) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized -from homeassistant.helpers import config_validation as cv, event, template +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + event, + template, +) +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.frame import report @@ -76,6 +83,7 @@ CONF_TOPIC, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, + DATA_MQTT_RELOAD_NEEDED, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_ENCODING, @@ -150,6 +158,7 @@ Platform.SELECT, Platform.SCENE, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, Platform.VACUUM, ] @@ -170,49 +179,53 @@ required=True, ) +CONFIG_SCHEMA_BASE = vol.Schema( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any( + "auto", "1.0", "1.1", "1.2" + ), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY, default=DEFAULT_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, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - cv.deprecated(CONF_TLS_VERSION), - vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional( - CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL - ): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional( - CONF_WILL_MESSAGE, default=DEFAULT_WILL - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional( - CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_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, default=DEFAULT_PREFIX - ): valid_publish_topic, - } - ), + cv.deprecated(CONF_BIRTH_MESSAGE), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_BROKER), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_DISCOVERY), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_PASSWORD), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_PORT), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_TLS_VERSION), # Deprecated June 2020 + cv.deprecated(CONF_USERNAME), # Deprecated in HA Core 2022.3 + cv.deprecated(CONF_WILL_MESSAGE), # Deprecated in HA Core 2022.3 + CONFIG_SCHEMA_BASE, ) }, extra=vol.ALLOW_EXTRA, @@ -579,23 +592,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_mqtt_info) debug_info.initialize(hass) - if conf is None: - # If we have a config entry, setup is done by that config entry. - # If there is no config entry, this should fail. - return bool(hass.config_entries.async_entries(DOMAIN)) - - conf = dict(conf) - - hass.data[DATA_MQTT_CONFIG] = conf + if conf: + conf = dict(conf) + hass.data[DATA_MQTT_CONFIG] = conf - # Only import if we haven't before. - if not hass.config_entries.async_entries(DOMAIN): + if not bool(hass.config_entries.async_entries(DOMAIN)): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, ) ) - return True @@ -606,17 +614,9 @@ def _merge_config(entry, conf): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - conf = hass.data.get(DATA_MQTT_CONFIG) - - # Config entry was created because user had configuration.yaml entry - # They removed that, so remove entry. - if conf is None and entry.source == config_entries.SOURCE_IMPORT: - hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) - return False - # If user didn't have configuration.yaml config, generate defaults - if conf is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() override = {k: entry.data[k] for k in shared_keys} @@ -734,6 +734,15 @@ async def finish_dump(_): if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) + if DATA_MQTT_RELOAD_NEEDED in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=False, + ) + return True @@ -799,7 +808,7 @@ async def async_config_entry_updated( self = hass.data[DATA_MQTT] if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) self.conf = _merge_config(entry, conf) await self.async_disconnect() @@ -894,7 +903,7 @@ async def async_publish( async def async_connect(self) -> None: """Connect to the host. Does not process messages yet.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt result: int | None = None @@ -958,10 +967,6 @@ def async_remove() -> None: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - # Only unsubscribe if currently connected. if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -973,6 +978,10 @@ async def _async_unsubscribe(self, topic: str) -> None: This method is a coroutine. """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + async with self._paho_lock: result: int | None = None result, mid = await self.hass.async_add_executor_job( @@ -999,7 +1008,7 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: Resubscribe to all topics we were subscribed to and publish birth message. """ - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt if result_code != mqtt.CONNACK_ACCEPTED: @@ -1158,7 +1167,7 @@ async def _discovery_cooldown(self): def _raise_on_error(result_code: int | None) -> None: """Raise error if error result.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt if result_code is not None and result_code != 0: @@ -1168,7 +1177,7 @@ def _raise_on_error(result_code: int | None) -> None: def _matcher_for_topic(subscription: str) -> Any: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from paho.mqtt.matcher import MQTTMatcher matcher = MQTTMatcher() @@ -1180,11 +1189,11 @@ def _matcher_for_topic(subscription: str) -> Any: @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) -@websocket_api.async_response -async def websocket_mqtt_info(hass, connection, msg): +@callback +def websocket_mqtt_info(hass, connection, msg): """Get MQTT debug info for device.""" device_id = msg["device_id"] - mqtt_info = await debug_info.info_for_device(hass, device_id) + mqtt_info = debug_info.info_for_device(hass, device_id) connection.send_result(msg["id"], mqtt_info) @@ -1196,9 +1205,9 @@ async def websocket_mqtt_info(hass, connection, msg): async def websocket_remove_device(hass, connection, msg): """Delete device.""" device_id = msg["device_id"] - dev_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) - if not (device := dev_registry.async_get(device_id)): + if not (device := device_registry.async_get(device_id)): connection.send_error( msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" ) @@ -1208,7 +1217,10 @@ async def websocket_remove_device(hass, connection, msg): config_entry = hass.config_entries.async_get_entry(config_entry) # Only delete the device if it belongs to an MQTT device entry if config_entry.domain == DOMAIN: - dev_registry.async_remove_device(device_id) + await async_remove_config_entry_device(hass, config_entry, device) + device_registry.async_update_device( + device_id, remove_config_entry_id=config_entry.entry_id + ) connection.send_message(websocket_api.result_message(msg["id"])) return @@ -1286,3 +1298,14 @@ def unsubscribe(): def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" return hass.data[DATA_MQTT].connected + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove MQTT config entry from a device.""" + # pylint: disable-next=import-outside-toplevel + from . import device_automation + + await device_automation.async_removed_from_device(hass, device_entry.id) + return True diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c5c70ad33a4bb..ddbced5286dae 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -7,6 +7,7 @@ "aux_cmd_t": "aux_command_topic", "aux_stat_tpl": "aux_state_template", "aux_stat_t": "aux_state_topic", + "av_tones": "available_tones", "avty": "availability", "avty_mode": "availability_mode", "avty_t": "availability_topic", @@ -15,6 +16,7 @@ "away_mode_stat_tpl": "away_mode_state_template", "away_mode_stat_t": "away_mode_state_topic", "b_tpl": "blue_template", + "bri_cmd_tpl": "brightness_command_template", "bri_cmd_t": "brightness_command_topic", "bri_scl": "brightness_scale", "bri_stat_t": "brightness_state_topic", @@ -49,6 +51,7 @@ "dock_tpl": "docked_template", "e": "encoding", "en": "enabled_by_default", + "ent_cat": "entity_category", "err_t": "error_topic", "err_tpl": "error_template", "fanspd_t": "fan_speed_topic", @@ -56,6 +59,7 @@ "fanspd_lst": "fan_speed_list", "flsh_tlng": "flash_time_long", "flsh_tsht": "flash_time_short", + "fx_cmd_tpl": "effect_command_template", "fx_cmd_t": "effect_command_topic", "fx_list": "effect_list", "fx_stat_t": "effect_state_topic", @@ -205,6 +209,8 @@ "stat_val_tpl": "state_value_template", "step": "step", "stype": "subtype", + "sup_dur": "support_duration", + "sup_vol": "support_volume_set", "sup_feat": "supported_features", "sup_clrm": "supported_color_modes", "swing_mode_cmd_tpl": "swing_mode_command_template", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 63c4a79b96fca..cca4e58658c4c 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -35,10 +35,9 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -47,10 +46,14 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) _LOGGER = logging.getLogger(__name__) @@ -124,8 +127,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT alarm control panel through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -173,7 +177,7 @@ def _setup_from_config(self, config): self._config[CONF_COMMAND_TEMPLATE], entity=self ).async_render - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -198,7 +202,7 @@ def message_received(msg): self._state = payload self.async_write_ha_state() - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -211,6 +215,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + @property def state(self): """Return the state of the device.""" @@ -324,8 +332,7 @@ async def _publish(self, code, action): """Publish via mqtt.""" variables = {"action": action, "code": code} payload = self._command_template(None, variables=variables) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5b41c916f8e3b..40d5c876c111d 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -29,20 +29,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import PLATFORMS, MqttValueTemplate, subscription +from . import MqttValueTemplate, subscription from .. import mqtt -from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, async_setup_entry_helper, + async_setup_platform_helper, ) _LOGGER = logging.getLogger(__name__) @@ -76,8 +76,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT binary sensor through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -167,7 +168,7 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -216,6 +217,8 @@ def state_message_received(msg): self._state = True elif payload == self._config[CONF_PAYLOAD_OFF]: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None else: # Payload is not for this entity template_info = "" if self._config.get(CONF_VALUE_TEMPLATE) is not None: @@ -241,7 +244,7 @@ def state_message_received(msg): self.async_write_ha_state() - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -254,6 +257,10 @@ def state_message_received(msg): }, ) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" @@ -263,7 +270,7 @@ def _value_is_expired(self, *_): self.async_write_ha_state() @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 7143b65ed9eda..22ee7b6d5aec6 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -12,10 +12,9 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate +from . import MqttCommandTemplate from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -23,9 +22,13 @@ CONF_ENCODING, CONF_QOS, CONF_RETAIN, - DOMAIN, ) -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" @@ -52,8 +55,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT button through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, button.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -95,6 +99,9 @@ def _setup_from_config(self, config): config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render + def _prepare_subscribe_topics(self): + """(Re)Subscribe to topics.""" + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -109,8 +116,7 @@ async def async_press(self, **kwargs): This method is a coroutine. """ payload = self._command_template(self._config[CONF_PAYLOAD_PRESS]) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5c2b8258f010d..0e387023a396d 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -12,14 +12,18 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, subscription +from . import subscription from .. import mqtt -from .const import CONF_QOS, CONF_TOPIC, DOMAIN +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 +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) DEFAULT_NAME = "MQTT Camera" @@ -49,8 +53,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT camera through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -90,7 +95,7 @@ def config_schema(): """Return the config schema.""" return DISCOVERY_SCHEMA - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -99,7 +104,7 @@ def message_received(msg): """Handle new MQTT messages.""" self._last_image = msg.payload - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -112,6 +117,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 2e19a345bc39e..e145edde7d774 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -53,20 +53,23 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ( MQTT_BASE_PLATFORM_SCHEMA, - PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription, ) from .. import mqtt -from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) _LOGGER = logging.getLogger(__name__) @@ -77,6 +80,7 @@ CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic" CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template" CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic" @@ -87,6 +91,7 @@ CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template" CONF_HOLD_COMMAND_TOPIC = "hold_command_topic" CONF_HOLD_STATE_TEMPLATE = "hold_state_template" @@ -101,7 +106,12 @@ CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" CONF_PRECISION = "precision" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 +CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" +CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" +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_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 CONF_SEND_IF_OFF = "send_if_off" CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" @@ -125,8 +135,6 @@ CONF_TEMP_MIN = "min_temp" CONF_TEMP_STEP = "temp_step" -PAYLOAD_NONE = "None" - MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { climate.ATTR_AUX_HEAT, @@ -154,13 +162,16 @@ VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_AWAY_MODE_STATE_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_HOLD_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE, CONF_ACTION_TEMPLATE, + CONF_PRESET_MODE_VALUE_TEMPLATE, CONF_SWING_MODE_STATE_TEMPLATE, CONF_TEMP_HIGH_STATE_TEMPLATE, CONF_TEMP_LOW_STATE_TEMPLATE, @@ -169,29 +180,48 @@ COMMAND_TEMPLATE_KEYS = { CONF_FAN_MODE_COMMAND_TEMPLATE, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_HOLD_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_SWING_MODE_COMMAND_TEMPLATE, CONF_TEMP_COMMAND_TEMPLATE, CONF_TEMP_HIGH_COMMAND_TEMPLATE, CONF_TEMP_LOW_COMMAND_TEMPLATE, } +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +DEPRECATED_INVALID = [ + CONF_AWAY_MODE_COMMAND_TOPIC, + CONF_AWAY_MODE_STATE_TEMPLATE, + CONF_AWAY_MODE_STATE_TOPIC, + CONF_HOLD_COMMAND_TEMPLATE, + CONF_HOLD_COMMAND_TOPIC, + CONF_HOLD_STATE_TEMPLATE, + CONF_HOLD_STATE_TOPIC, + CONF_HOLD_LIST, +] + + TOPIC_KEYS = ( + CONF_ACTION_TOPIC, CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_AWAY_MODE_COMMAND_TOPIC, CONF_AWAY_MODE_STATE_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 CONF_HOLD_COMMAND_TOPIC, CONF_HOLD_STATE_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC, - CONF_ACTION_TOPIC, + CONF_PRESET_MODE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, CONF_TEMP_COMMAND_TOPIC, @@ -202,12 +232,27 @@ CONF_TEMP_STATE_TOPIC, ) + +def valid_preset_mode_configuration(config): + """Validate that the preset mode reset payload is not one of the preset modes.""" + if PRESET_NONE in config.get(CONF_PRESET_MODES_LIST): + raise ValueError("preset_modes must not include preset mode 'none'") + if config.get(CONF_PRESET_MODE_COMMAND_TOPIC): + for config_parameter in DEPRECATED_INVALID: + if config.get(config_parameter): + raise vol.MultipleInvalid( + "preset_modes cannot be used with deprecated away or hold mode config options" + ) + return config + + SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( { vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -221,6 +266,7 @@ ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, @@ -251,10 +297,20 @@ [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + vol.Inclusive( + CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" + ): mqtt.valid_publish_topic, + vol.Inclusive( + CONF_PRESET_MODES_LIST, "preset_modes", default=[] + ): cv.ensure_list, + vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -284,17 +340,37 @@ ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA = vol.All( - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 - cv.deprecated(CONF_SEND_IF_OFF), _PLATFORM_SCHEMA_BASE, + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + cv.deprecated(CONF_SEND_IF_OFF), + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 + cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), + cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), + cv.deprecated(CONF_HOLD_COMMAND_TOPIC), + cv.deprecated(CONF_HOLD_STATE_TEMPLATE), + cv.deprecated(CONF_HOLD_STATE_TOPIC), + cv.deprecated(CONF_HOLD_LIST), + valid_preset_mode_configuration, ) _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA) DISCOVERY_SCHEMA = vol.All( - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 - cv.deprecated(CONF_SEND_IF_OFF), _DISCOVERY_SCHEMA_BASE, + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + cv.deprecated(CONF_SEND_IF_OFF), + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 + cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), + cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), + cv.deprecated(CONF_HOLD_COMMAND_TOPIC), + cv.deprecated(CONF_HOLD_STATE_TEMPLATE), + cv.deprecated(CONF_HOLD_STATE_TOPIC), + cv.deprecated(CONF_HOLD_LIST), + valid_preset_mode_configuration, ) @@ -305,8 +381,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT climate device through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -344,12 +421,15 @@ def __init__(self, hass, config, config_entry, discovery_data): self._current_swing_mode = None self._current_temp = None self._hold = 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 MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -358,11 +438,6 @@ def config_schema(): """Return the config schema.""" return DISCOVERY_SCHEMA - async def async_added_to_hass(self): - """Handle being added to Home Assistant.""" - await super().async_added_to_hass() - await self._subscribe_topics() - def _setup_from_config(self, config): """(Re)Setup the entity.""" self._topic = {key: config.get(key) for key in TOPIC_KEYS} @@ -387,7 +462,14 @@ def _setup_from_config(self, config): self._current_swing_mode = HVAC_MODE_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: self._current_operation = HVAC_MODE_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] + else: + self._preset_modes = [] + self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config self._action = None + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 self._away = False self._hold = None self._aux = False @@ -417,7 +499,7 @@ def _setup_from_config(self, config): self._command_templates = command_templates - async def _subscribe_topics(self): # noqa: C901 + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} qos = self._config[CONF_QOS] @@ -585,6 +667,7 @@ def handle_onoff_mode_received(msg, template_name, attr): self.async_write_ha_state() + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @callback @log_messages(self.hass, self.entity_id) def handle_away_mode_received(msg): @@ -601,6 +684,7 @@ def handle_aux_mode_received(msg): add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @callback @log_messages(self.hass, self.entity_id) def handle_hold_mode_received(msg): @@ -611,14 +695,46 @@ def handle_hold_mode_received(msg): payload = None self._hold = payload + self._preset_mode = None self.async_write_ha_state() add_subscription(topics, CONF_HOLD_STATE_TOPIC, handle_hold_mode_received) - self._sub_state = await subscription.async_subscribe_topics( + @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 + 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: + _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 + self.async_write_ha_state() + + 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): """Return the unit of measurement.""" @@ -667,8 +783,11 @@ def target_temperature_step(self): return self._config[CONF_TEMP_STEP] @property - def preset_mode(self): + 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 + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 if self._hold: return self._hold if self._away: @@ -676,10 +795,12 @@ def preset_mode(self): return PRESET_NONE @property - def preset_modes(self): + def preset_modes(self) -> list: """Return preset modes.""" presets = [] + presets.extend(self._preset_modes) + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or ( self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None ): @@ -709,8 +830,7 @@ def fan_modes(self): async def _publish(self, topic, payload): if self._topic[topic] is not None: - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[topic], payload, self._config[CONF_QOS], @@ -726,7 +846,7 @@ async def _set_temperature( # optimistic mode setattr(self, attr, temp) - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if ( self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF @@ -769,7 +889,7 @@ async def async_set_temperature(self, **kwargs): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode @@ -782,7 +902,7 @@ async def async_set_swing_mode(self, swing_mode): async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.4 + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) @@ -817,11 +937,29 @@ def swing_modes(self): """List of available swing modes.""" return self._config[CONF_SWING_MODE_LIST] - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set a preset mode.""" - # Track if we should optimistic update the state + 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 + + # Update hold or away mode: Track if we should optimistic update the state optimistic_update = await self._set_away_mode(preset_mode == PRESET_AWAY) - hold_mode = preset_mode + hold_mode: str | None = preset_mode if preset_mode in [PRESET_NONE, PRESET_AWAY]: hold_mode = None optimistic_update = await self._set_hold_mode(hold_mode) or optimistic_update @@ -829,6 +967,7 @@ async def async_set_preset_mode(self, preset_mode): if optimistic_update: self.async_write_ha_state() + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def _set_away_mode(self, state): """Set away mode. @@ -909,8 +1048,10 @@ def supported_features(self): ): support |= SUPPORT_SWING_MODE + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 if ( - (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) + self._feature_preset_mode + or (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or (self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None) or (self._topic[CONF_HOLD_STATE_TOPIC] is not None) or (self._topic[CONF_HOLD_COMMAND_TOPIC] is not None) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 84322ddc1eefc..3f93e50829a8a 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -33,6 +33,8 @@ ) from .util import MQTT_WILL_BIRTH_SCHEMA +MQTT_TIMEOUT = 5 + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -84,17 +86,6 @@ async def async_step_broker(self, user_input=None): step_id="broker", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_import(self, user_input): - """Import a config entry. - - Special type of import, we're not actually going to store any data. - Instead, we're going to rely on the values that are in config file. - """ - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry(title="configuration.yaml", data={}) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Receive a Hass.io discovery.""" await self._async_handle_discovery_without_unique_id() @@ -324,7 +315,7 @@ async def async_step_options(self, user_input=None): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt if protocol == "3.1": @@ -348,7 +339,7 @@ def on_connect(client_, userdata, flags, result_code): client.loop_start() try: - return result.get(timeout=5) + return result.get(timeout=MQTT_TIMEOUT) except queue.Empty: return False finally: diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 4ccd81904b14b..f04348ee0020b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -18,10 +18,12 @@ CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" +CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" CONF_WILL_MESSAGE = "will_message" DATA_MQTT_CONFIG = "mqtt_config" +DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" @@ -51,4 +53,7 @@ MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" +PAYLOAD_EMPTY_JSON = "{}" +PAYLOAD_NONE = "None" + PROTOCOL_311 = "3.1.1" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 95ea6182bf18d..282e57fea9eb9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -37,10 +37,9 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -48,10 +47,14 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) _LOGGER = logging.getLogger(__name__) @@ -217,8 +220,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT cover through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -335,7 +339,7 @@ def _setup_from_config(self, config): config_attributes=template_config_attributes, ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -460,10 +464,14 @@ def position_message_received(msg): "encoding": self._config[CONF_ENCODING] or None, } - self._sub_state = await subscription.async_subscribe_topics( + 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 assumed_state(self): """Return true if we do optimistic updates.""" @@ -530,8 +538,7 @@ async def async_open_cover(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_OPEN], self._config[CONF_QOS], @@ -552,8 +559,7 @@ async def async_close_cover(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_CLOSE], self._config[CONF_QOS], @@ -574,8 +580,7 @@ async def async_stop_cover(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_COMMAND_TOPIC), self._config[CONF_PAYLOAD_STOP], self._config[CONF_QOS], @@ -595,8 +600,7 @@ async def async_open_cover_tilt(self, **kwargs): "tilt_max": self._config.get(CONF_TILT_MAX), } tilt_payload = self._set_tilt_template(tilt_open_position, variables=variables) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_TILT_COMMAND_TOPIC), tilt_payload, self._config[CONF_QOS], @@ -623,8 +627,7 @@ async def async_close_cover_tilt(self, **kwargs): tilt_payload = self._set_tilt_template( tilt_closed_position, variables=variables ) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_TILT_COMMAND_TOPIC), tilt_payload, self._config[CONF_QOS], @@ -653,8 +656,7 @@ async def async_set_cover_tilt_position(self, **kwargs): } tilt = self._set_tilt_template(tilt, variables=variables) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_TILT_COMMAND_TOPIC), tilt, self._config[CONF_QOS], @@ -681,8 +683,7 @@ async def async_set_cover_position(self, **kwargs): } position = self._set_position_template(position, variables=variables) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config.get(CONF_SET_POSITION_TOPIC), position, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 2b9172f0c9c37..17dbc27f0c4ba 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -3,13 +3,18 @@ from collections import deque from collections.abc import Callable +import datetime as dt from functools import wraps from typing import Any +import attr + from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC -from .models import MessageCallbackType +from .models import MessageCallbackType, PublishPayloadType DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 @@ -47,12 +52,46 @@ def wrapper(msg: Any) -> None: return _decorator +@attr.s(slots=True, frozen=True) +class TimestampedPublishMessage: + """MQTT Message.""" + + topic: str = attr.ib() + payload: PublishPayloadType = attr.ib() + qos: int = attr.ib() + retain: bool = attr.ib() + timestamp: dt.datetime = attr.ib(default=None) + + +def log_message( + hass: HomeAssistant, + entity_id: str, + topic: str, + payload: PublishPayloadType, + qos: int, + retain: bool, +) -> None: + """Log an outgoing MQTT message.""" + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + entity_info = debug_info["entities"].setdefault( + entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} + ) + if topic not in entity_info["transmitted"]: + entity_info["transmitted"][topic] = { + "messages": deque([], STORED_MESSAGES), + } + msg = TimestampedPublishMessage( + topic, payload, qos, retain, timestamp=dt_util.utcnow() + ) + entity_info["transmitted"][topic]["messages"].append(msg) + + def add_subscription(hass, message_callback, subscription): """Prepare debug data for subscription.""" if entity_id := getattr(message_callback, "__entity_id", None): debug_info = hass.data[DATA_MQTT_DEBUG_INFO] entity_info = debug_info["entities"].setdefault( - entity_id, {"subscriptions": {}, "discovery_data": {}} + entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) if subscription not in entity_info["subscriptions"]: entity_info["subscriptions"][subscription] = { @@ -81,7 +120,7 @@ def add_entity_discovery_data(hass, discovery_data, entity_id): """Add discovery data.""" debug_info = hass.data[DATA_MQTT_DEBUG_INFO] entity_info = debug_info["entities"].setdefault( - entity_id, {"subscriptions": {}, "discovery_data": {}} + entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) entity_info["discovery_data"] = discovery_data @@ -115,15 +154,89 @@ def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): def remove_trigger_discovery_data(hass, discovery_hash): """Remove discovery data.""" - hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash]["discovery_data"] = None + hass.data[DATA_MQTT_DEBUG_INFO]["triggers"].pop(discovery_hash) + + +def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: + mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + entity_info = mqtt_debug_info["entities"][entity_id] + subscriptions = [ + { + "topic": topic, + "messages": [ + { + "payload": str(msg.payload), + "qos": msg.qos, + "retain": msg.retain, + "time": msg.timestamp, + "topic": msg.topic, + } + for msg in subscription["messages"] + ], + } + for topic, subscription in entity_info["subscriptions"].items() + ] + transmitted = [ + { + "topic": topic, + "messages": [ + { + "payload": str(msg.payload), + "qos": msg.qos, + "retain": msg.retain, + "time": msg.timestamp, + "topic": msg.topic, + } + for msg in subscription["messages"] + ], + } + for topic, subscription in entity_info["transmitted"].items() + ] + discovery_data = { + "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), + "payload": entity_info["discovery_data"].get(ATTR_DISCOVERY_PAYLOAD, ""), + } + + return { + "entity_id": entity_id, + "subscriptions": subscriptions, + "discovery_data": discovery_data, + "transmitted": transmitted, + } + + +def _info_for_trigger(hass: HomeAssistant, trigger_key: str) -> dict[str, Any]: + mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + trigger = mqtt_debug_info["triggers"][trigger_key] + discovery_data = None + if trigger["discovery_data"] is not None: + discovery_data = { + "topic": trigger["discovery_data"][ATTR_DISCOVERY_TOPIC], + "payload": trigger["discovery_data"][ATTR_DISCOVERY_PAYLOAD], + } + return {"discovery_data": discovery_data, "trigger_key": trigger_key} + + +def info_for_config_entry(hass): + """Get debug info for all entities and triggers.""" + mqtt_info = {"entities": [], "triggers": []} + mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + + for entity_id in mqtt_debug_info["entities"]: + mqtt_info["entities"].append(_info_for_entity(hass, entity_id)) + + for trigger_key in mqtt_debug_info["triggers"]: + mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) + + return mqtt_info -async def info_for_device(hass, device_id): +def info_for_device(hass, device_id): """Get debug info for a device.""" mqtt_info = {"entities": [], "triggers": []} - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) - entries = hass.helpers.entity_registry.async_entries_for_device( + entries = er.async_entries_for_device( entity_registry, device_id, include_disabled_entities=True ) mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] @@ -131,43 +244,12 @@ async def info_for_device(hass, device_id): if entry.entity_id not in mqtt_debug_info["entities"]: continue - entity_info = mqtt_debug_info["entities"][entry.entity_id] - subscriptions = [ - { - "topic": topic, - "messages": [ - { - "payload": str(msg.payload), - "qos": msg.qos, - "retain": msg.retain, - "time": msg.timestamp, - "topic": msg.topic, - } - for msg in list(subscription["messages"]) - ], - } - for topic, subscription in entity_info["subscriptions"].items() - ] - discovery_data = { - "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), - "payload": entity_info["discovery_data"].get(ATTR_DISCOVERY_PAYLOAD, ""), - } - mqtt_info["entities"].append( - { - "entity_id": entry.entity_id, - "subscriptions": subscriptions, - "discovery_data": discovery_data, - } - ) + mqtt_info["entities"].append(_info_for_entity(hass, entry.entity_id)) - for trigger in mqtt_debug_info["triggers"].values(): - if trigger["device_id"] != device_id or trigger["discovery_data"] is None: + for trigger_key, trigger in mqtt_debug_info["triggers"].items(): + if trigger["device_id"] != device_id: continue - discovery_data = { - "topic": trigger["discovery_data"][ATTR_DISCOVERY_TOPIC], - "payload": trigger["discovery_data"][ATTR_DISCOVERY_PAYLOAD], - } - mqtt_info["triggers"].append({"discovery_data": discovery_data}) + mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) return mqtt_info diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 50d6a6e4d1958..cafbd66b09863 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,8 +3,6 @@ import voluptuous as vol -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED - from . import device_trigger from .. import mqtt from .mixins import async_setup_entry_helper @@ -23,15 +21,8 @@ async def async_setup_entry(hass, config_entry): """Set up MQTT device automation dynamically through MQTT discovery.""" - async def async_device_removed(event): - """Handle the removal of a device.""" - if event.data["action"] != "remove": - return - await device_trigger.async_device_removed(hass, event.data["device_id"]) - setup = functools.partial(_async_setup_automation, hass, config_entry=config_entry) await async_setup_entry_helper(hass, "device_automation", setup, PLATFORM_SCHEMA) - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) async def _async_setup_automation(hass, config, config_entry, discovery_data): @@ -40,3 +31,8 @@ async def _async_setup_automation(hass, config, config_entry, discovery_data): await device_trigger.async_setup_trigger( hass, config, config_entry, discovery_data ) + + +async def async_removed_from_device(hass, device_id): + """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/schema_discovery.py index 3ee5f22be902e..a7b597d06897c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -77,7 +77,7 @@ def _setup_from_config(self, config): self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -94,7 +94,7 @@ def message_received(msg): self.async_write_ha_state() - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -106,6 +106,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + @property def latitude(self): """Return latitude if provided in extra_state_attributes or None.""" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 78f52e5872614..71c0a9f93645b 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -23,7 +23,7 @@ ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -190,9 +190,9 @@ def detach_trigger(self): trig.remove = None -async def _update_device(hass, config_entry, config): +def _update_device(hass, config_entry, config): """Update device registry.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) config_entry_id = config_entry.entry_id device_info = device_info_from_config(config[CONF_DEVICE]) @@ -222,13 +222,13 @@ async def discovery_update(payload): device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) remove_signal() - await cleanup_device_registry(hass, device.id) + await cleanup_device_registry(hass, device.id, config_entry.entry_id) else: # Non-empty payload: Update trigger _LOGGER.info("Updating trigger: %s", discovery_hash) debug_info.update_trigger_discovery_data(hass, discovery_hash, payload) config = TRIGGER_DISCOVERY_SCHEMA(payload) - await _update_device(hass, config_entry, config) + _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] await device_trigger.update_trigger(config, discovery_hash, remove_signal) async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) @@ -237,9 +237,9 @@ async def discovery_update(payload): hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update ) - await _update_device(hass, config_entry, config) + _update_device(hass, config_entry, config) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( {(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]}, {tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]}, @@ -275,8 +275,8 @@ async def discovery_update(payload): async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) -async def async_device_removed(hass: HomeAssistant, device_id: str): - """Handle the removal of a device.""" +async def async_removed_from_device(hass: HomeAssistant, device_id: str): + """Handle Mqtt removed from a device.""" triggers = await async_get_triggers(hass, device_id) for trig in triggers: device_trigger = hass.data[DEVICE_TRIGGERS].pop(trig[CONF_DISCOVERY_ID]) diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py new file mode 100644 index 0000000000000..ea490783fc09f --- /dev/null +++ b/homeassistant/components/mqtt/diagnostics.py @@ -0,0 +1,126 @@ +"""Diagnostics support for MQTT.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components import device_tracker +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntry + +from . import DATA_MQTT, MQTT, debug_info, is_connected + +REDACT_CONFIG = {CONF_PASSWORD, CONF_USERNAME} +REDACT_STATE_DEVICE_TRACKER = {ATTR_LATITUDE, ATTR_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return _async_get_diagnostics(hass, entry) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + return _async_get_diagnostics(hass, entry, device) + + +@callback +def _async_get_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, + device: DeviceEntry | None = None, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + mqtt_instance: MQTT = hass.data[DATA_MQTT] + + redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG) + + data = { + "connected": is_connected(hass), + "mqtt_config": redacted_config, + } + + if device: + data["device"] = _async_device_as_dict(hass, device) + data["mqtt_debug_info"] = debug_info.info_for_device(hass, device.id) + else: + device_registry = dr.async_get(hass) + data.update( + devices=[ + _async_device_as_dict(hass, device) + for device in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + ], + mqtt_debug_info=debug_info.info_for_config_entry(hass), + ) + + return data + + +@callback +def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str, Any]: + """Represent an MQTT device as a dictionary.""" + + # Gather information how this MQTT device is represented in Home Assistant + entity_registry = er.async_get(hass) + data: dict[str, Any] = { + "id": device.id, + "name": device.name, + "name_by_user": device.name_by_user, + "disabled": device.disabled, + "disabled_by": device.disabled_by, + "entities": [], + } + + entities = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + for entity_entry in entities: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) + + entity_domain = split_entity_id(state.entity_id)[0] + + # Retract some sensitive state attributes + if entity_domain == device_tracker.DOMAIN: + state_dict["attributes"] = async_redact_data( + state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER + ) + + data["entities"].append( + { + "device_class": entity_entry.device_class, + "disabled_by": entity_entry.disabled_by, + "disabled": entity_entry.disabled, + "entity_category": entity_entry.entity_category, + "entity_id": entity_entry.entity_id, + "icon": entity_entry.icon, + "original_device_class": entity_entry.original_device_class, + "original_icon": entity_entry.original_icon, + "state": state_dict, + "unit_of_measurement": entity_entry.unit_of_measurement, + } + ) + + return data diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index c9b0b816c4efa..11bc0f6839a29 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -50,6 +50,7 @@ "lock", "number", "scene", + "siren", "select", "sensor", "switch", @@ -227,13 +228,13 @@ async def discovery_done(_): if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: if component == "device_automation": # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import device_automation await device_automation.async_setup_entry(hass, config_entry) elif component == "tag": # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import tag await tag.async_setup_entry(hass, config_entry) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index fb6d21c853817..bedc3467c3b17 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -32,7 +32,6 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.percentage import ( int_states_in_range, @@ -40,7 +39,7 @@ ranged_value_to_percentage, ) -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -49,12 +48,17 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, + CONF_STATE_VALUE_TEMPLATE, + PAYLOAD_NONE, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) -CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" @@ -212,8 +216,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT fan through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -243,7 +248,7 @@ class MqttFan(MqttEntity, FanEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" - self._state = False + self._state = None self._percentage = None self._preset_mode = None self._oscillation = None @@ -351,7 +356,7 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -367,6 +372,8 @@ def state_received(msg): self._state = True elif payload == self._payload["STATE_OFF"]: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -479,17 +486,21 @@ def oscillation_received(msg): } self._oscillation = False - self._sub_state = await subscription.async_subscribe_topics( + 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 assumed_state(self): """Return true if we do optimistic updates.""" return self._optimistic @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state @@ -536,8 +547,7 @@ async def async_turn_on( This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -558,8 +568,7 @@ async def async_turn_off(self, **kwargs) -> None: This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -579,8 +588,7 @@ async def async_set_percentage(self, percentage: int) -> None: percentage_to_ranged_value(self._speed_range, percentage) ) mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -603,8 +611,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -630,8 +637,7 @@ async def async_oscillate(self, oscillating: bool) -> None: self._payload["OSCILLATE_OFF_PAYLOAD"] ) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_OSCILLATION_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index df1b7667ef751..99674051521cb 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -27,10 +27,9 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -39,10 +38,16 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, + CONF_STATE_VALUE_TEMPLATE, + PAYLOAD_NONE, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -52,7 +57,6 @@ CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_PAYLOAD_RESET_MODE = "payload_reset_mode" CONF_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity" -CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" CONF_TARGET_HUMIDITY_MIN = "min_humidity" @@ -156,8 +160,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT humidifier through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -187,7 +192,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT humidifier.""" - self._state = False + self._state = None self._target_humidity = None self._mode = None self._supported_features = 0 @@ -267,7 +272,7 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -283,6 +288,8 @@ def state_received(msg): self._state = True elif payload == self._payload["STATE_OFF"]: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -373,10 +380,14 @@ def mode_received(msg): } self._mode = None - self._sub_state = await subscription.async_subscribe_topics( + 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 assumed_state(self): """Return true if we do optimistic updates.""" @@ -388,7 +399,7 @@ def available_modes(self) -> list: return self._available_modes @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state @@ -411,8 +422,7 @@ async def async_turn_on( This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -429,8 +439,7 @@ async def async_turn_off(self, **kwargs) -> None: This method is a coroutine. """ mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -447,8 +456,7 @@ async def async_set_humidity(self, humidity: int) -> None: This method is a coroutine. """ mqtt_payload = self._command_templates[ATTR_HUMIDITY](humidity) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_TARGET_HUMIDITY_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], @@ -471,8 +479,7 @@ async def async_set_mode(self, mode: str) -> None: mqtt_payload = self._command_templates[ATTR_MODE](mode) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_MODE_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 09dfb8417ccc5..d78cd5e7baa03 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -8,11 +8,9 @@ from homeassistant.components import light from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .. import DOMAIN, PLATFORMS -from ..mixins import async_setup_entry_helper +from ..mixins import async_setup_entry_helper, async_setup_platform_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( DISCOVERY_SCHEMA_BASIC, @@ -69,8 +67,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT light through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, light.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 4f692c8063c76..09b23029b0e7f 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -59,6 +59,8 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_STATE_VALUE_TEMPLATE, + PAYLOAD_NONE, ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity @@ -66,6 +68,7 @@ _LOGGER = logging.getLogger(__name__) +CONF_BRIGHTNESS_COMMAND_TEMPLATE = "brightness_command_template" CONF_BRIGHTNESS_COMMAND_TOPIC = "brightness_command_topic" CONF_BRIGHTNESS_SCALE = "brightness_scale" CONF_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic" @@ -76,6 +79,7 @@ CONF_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic" CONF_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic" CONF_COLOR_TEMP_VALUE_TEMPLATE = "color_temp_value_template" +CONF_EFFECT_COMMAND_TEMPLATE = "effect_command_template" CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic" CONF_EFFECT_LIST = "effect_list" CONF_EFFECT_STATE_TOPIC = "effect_state_topic" @@ -97,7 +101,6 @@ CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" -CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" CONF_XY_VALUE_TEMPLATE = "xy_value_template" @@ -140,7 +143,9 @@ VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] COMMAND_TEMPLATE_KEYS = [ + CONF_BRIGHTNESS_COMMAND_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, + CONF_EFFECT_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE, CONF_RGBW_COMMAND_TEMPLATE, CONF_RGBWW_COMMAND_TEMPLATE, @@ -162,6 +167,7 @@ _PLATFORM_SCHEMA_BASE = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE @@ -174,6 +180,7 @@ vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -257,7 +264,7 @@ def __init__(self, hass, config, config_entry, discovery_data): self._rgb_color = None self._rgbw_color = None self._rgbww_color = None - self._state = False + self._state = None self._supported_color_modes = None self._white_value = None self._xy_color = None @@ -417,12 +424,10 @@ def _is_optimistic(self, attribute): """Return True if the attribute is optimistically updated.""" return getattr(self, f"_optimistic_{attribute}") - async def _subscribe_topics(self): # noqa: C901 + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} - last_state = await self.async_get_last_state() - def add_topic(topic, msg_callback): """Add a topic.""" if self._topic[topic] is not None: @@ -433,21 +438,11 @@ def add_topic(topic, msg_callback): "encoding": self._config[CONF_ENCODING] or None, } - def restore_state(attribute, condition_attribute=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]) - @callback @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" - payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE]( - msg.payload, None - ) + payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) return @@ -456,6 +451,8 @@ def state_received(msg): self._state = True elif payload == self._payload["off"]: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -465,8 +462,6 @@ def state_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - elif self._optimistic and last_state: - self._state = last_state.state == STATE_ON @callback @log_messages(self.hass, self.entity_id) @@ -485,7 +480,6 @@ def brightness_received(msg): self.async_write_ha_state() add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) - restore_state(ATTR_BRIGHTNESS) def _rgbx_received(msg, template, color_mode, convert_color): """Handle new MQTT messages for RGBW and RGBWW.""" @@ -520,8 +514,6 @@ def rgb_received(msg): self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) - restore_state(ATTR_RGB_COLOR) - restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -539,7 +531,6 @@ def rgbw_received(msg): self.async_write_ha_state() add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) - restore_state(ATTR_RGBW_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -557,7 +548,6 @@ def rgbww_received(msg): self.async_write_ha_state() add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) - restore_state(ATTR_RGBWW_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -574,7 +564,6 @@ def color_mode_received(msg): self.async_write_ha_state() add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) - restore_state(ATTR_COLOR_MODE) @callback @log_messages(self.hass, self.entity_id) @@ -593,7 +582,6 @@ def color_temp_received(msg): self.async_write_ha_state() add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) - restore_state(ATTR_COLOR_TEMP) @callback @log_messages(self.hass, self.entity_id) @@ -610,7 +598,6 @@ def effect_received(msg): self.async_write_ha_state() add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) - restore_state(ATTR_EFFECT) @callback @log_messages(self.hass, self.entity_id) @@ -630,7 +617,6 @@ def hs_received(msg): _LOGGER.debug("Failed to parse hs state update: '%s'", payload) add_topic(CONF_HS_STATE_TOPIC, hs_received) - restore_state(ATTR_HS_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -649,7 +635,6 @@ def white_value_received(msg): self.async_write_ha_state() add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) - restore_state(ATTR_WHITE_VALUE) @callback @log_messages(self.hass, self.entity_id) @@ -670,13 +655,39 @@ def xy_received(msg): self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) - restore_state(ATTR_XY_COLOR) - restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) - self._sub_state = await subscription.async_subscribe_topics( + 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) + last_state = await self.async_get_last_state() + + def restore_state(attribute, condition_attribute=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]) + + if self._topic[CONF_STATE_TOPIC] is None and self._optimistic and last_state: + self._state = last_state.state == STATE_ON + restore_state(ATTR_BRIGHTNESS) + restore_state(ATTR_RGB_COLOR) + restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) + restore_state(ATTR_RGBW_COLOR) + restore_state(ATTR_RGBWW_COLOR) + restore_state(ATTR_COLOR_MODE) + restore_state(ATTR_COLOR_TEMP) + restore_state(ATTR_EFFECT) + restore_state(ATTR_HS_COLOR) + restore_state(ATTR_WHITE_VALUE) + restore_state(ATTR_XY_COLOR) + restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -827,8 +838,7 @@ async def async_turn_on(self, **kwargs): # noqa: C901 async def publish(topic, payload): """Publish an MQTT message.""" - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[topic], payload, self._config[CONF_QOS], @@ -966,6 +976,8 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): ) # 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) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( @@ -1034,8 +1046,10 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): 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}) await publish(CONF_EFFECT_COMMAND_TOPIC, effect) - should_update |= set_optimistic(ATTR_EFFECT, 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 @@ -1075,8 +1089,7 @@ async def async_turn_off(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], self._payload["off"], self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 3adaec38adbb9..83d40ed5aaec2 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -179,7 +179,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" - self._state = False + self._state = None self._supported_features = 0 self._topic = None @@ -304,9 +304,8 @@ def _update_color(self, values): except (KeyError, ValueError): _LOGGER.warning("Invalid or incomplete color value received") - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" - last_state = await self.async_get_last_state() @callback @log_messages(self.hass, self.entity_id) @@ -318,6 +317,8 @@ def state_received(msg): self._state = True elif values["state"] == "OFF": self._state = False + elif values["state"] is None: + self._state = None if self._supported_features and SUPPORT_COLOR and "color" in values: if values["color"] is None: @@ -370,7 +371,7 @@ def state_received(msg): self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -383,6 +384,11 @@ def state_received(msg): }, ) + async def _subscribe_topics(self): + """(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 last_attributes = last_state.attributes @@ -630,8 +636,7 @@ async def async_turn_on(self, **kwargs): # noqa: C901 self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self._config[CONF_QOS], @@ -656,8 +661,7 @@ async def async_turn_off(self, **kwargs): self._set_flash_and_transition(message, **kwargs) - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 54252ebc0b092..4f25bde928dfd 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -41,6 +41,7 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + PAYLOAD_NONE, ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity @@ -109,7 +110,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" - self._state = False + self._state = None self._topics = None self._templates = None @@ -156,14 +157,12 @@ def _setup_from_config(self, config): or self._templates[CONF_STATE_TEMPLATE] is None ) - async def _subscribe_topics(self): # noqa: C901 + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: tpl = MqttValueTemplate(tpl, entity=self) - last_state = await self.async_get_last_state() - @callback @log_messages(self.hass, self.entity_id) def state_received(msg): @@ -175,6 +174,8 @@ def state_received(msg): self._state = True elif state == STATE_OFF: self._state = False + elif state == PAYLOAD_NONE: + self._state = None else: _LOGGER.warning("Invalid state value received") @@ -246,7 +247,7 @@ def state_received(msg): self.async_write_ha_state() if self._topics[CONF_STATE_TOPIC] is not None: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -259,6 +260,11 @@ def state_received(msg): }, ) + async def _subscribe_topics(self): + """(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 if last_state.attributes.get(ATTR_BRIGHTNESS): @@ -385,8 +391,7 @@ async def async_turn_on(self, **kwargs): if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topics[CONF_COMMAND_TOPIC], self._templates[CONF_COMMAND_ON_TEMPLATE].async_render( parse_result=False, **values @@ -411,8 +416,7 @@ async def async_turn_off(self, **kwargs): if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] - await mqtt.async_publish( - self.hass, + await self.async_publish( self._topics[CONF_COMMAND_TOPIC], self._templates[CONF_COMMAND_OFF_TEMPLATE].async_render( parse_result=False, **values diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 1c280405522f6..1dd73175b3b88 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -12,10 +12,9 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttValueTemplate, subscription +from . import MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -23,10 +22,14 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -73,8 +76,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT lock panel through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -123,7 +127,7 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -142,7 +146,7 @@ def message_received(msg): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -155,6 +159,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + @property def is_locked(self): """Return true if lock is locked.""" @@ -175,8 +183,7 @@ async def async_lock(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_LOCK], self._config[CONF_QOS], @@ -193,8 +200,7 @@ async def async_unlock(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_UNLOCK], self._config[CONF_QOS], @@ -211,8 +217,7 @@ async def async_open(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OPEN], self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 6b92ab91e312d..9f3722a8f3174 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -5,9 +5,11 @@ from collections.abc import Callable import json import logging +from typing import Any, Protocol import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, ATTR_MANUFACTURER, @@ -23,8 +25,12 @@ CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -36,9 +42,18 @@ async_generate_entity_id, validate_entity_category, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import DATA_MQTT, MqttValueTemplate, debug_info, publish, subscription +from . import ( + DATA_MQTT, + PLATFORMS, + MqttValueTemplate, + async_publish, + debug_info, + subscription, +) from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -47,13 +62,15 @@ CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT_RELOAD_NEEDED, + DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, ) -from .debug_info import log_messages +from .debug_info import log_message, log_messages from .discovery import ( MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, @@ -61,8 +78,12 @@ clear_discovery_hash, set_discovery_hash, ) -from .models import ReceiveMessage -from .subscription import async_subscribe_topics, async_unsubscribe_topics +from .models import PublishPayloadType, ReceiveMessage +from .subscription import ( + async_prepare_subscribe_topics, + async_subscribe_topics, + async_unsubscribe_topics, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -201,6 +222,20 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType ) +class SetupEntity(Protocol): + """Protocol type for async_setup_entities.""" + + async def __call__( + self, + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict[str, Any] | None = None, + ) -> None: + """Define setup_entities type.""" + + async def async_setup_entry_helper(hass, domain, async_setup, schema): """Set up entity, automation or tag creation dynamically through MQTT discovery.""" @@ -223,6 +258,26 @@ async def async_discover(discovery_payload): ) +async def async_setup_platform_helper( + hass: HomeAssistant, + platform_domain: str, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + async_setup_entities: SetupEntity, +) -> None: + """Return true if platform setup should be aborted.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + if not bool(hass.config_entries.async_entries(DOMAIN)): + hass.data[DATA_MQTT_RELOAD_NEEDED] = None + _LOGGER.warning( + "MQTT integration is not setup, skipping setup of manually configured " + "MQTT %s", + platform_domain, + ) + return + await async_setup_entities(hass, async_add_entities, config) + + def init_entity_id_from_config(hass, entity, config, entity_id_format): """Set entity_id from object_id if defined in config.""" if CONF_OBJECT_ID in config: @@ -245,14 +300,19 @@ def __init__(self, config: dict) -> None: async def async_added_to_hass(self) -> None: """Subscribe MQTT events.""" await super().async_added_to_hass() + self._attributes_prepare_subscribe_topics() await self._attributes_subscribe_topics() - async def attributes_discovery_update(self, config: dict): + def attributes_prepare_discovery_update(self, config: dict): """Handle updated discovery message.""" self._attributes_config = config + self._attributes_prepare_subscribe_topics() + + async def attributes_discovery_update(self, config: dict): + """Handle updated discovery message.""" await self._attributes_subscribe_topics() - async def _attributes_subscribe_topics(self): + def _attributes_prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" attr_tpl = MqttValueTemplate( self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE), entity=self @@ -280,7 +340,7 @@ def attributes_message_received(msg: ReceiveMessage) -> None: _LOGGER.warning("Erroneous JSON: %s", payload) self._attributes = None - self._attributes_sub_state = await async_subscribe_topics( + self._attributes_sub_state = async_prepare_subscribe_topics( self.hass, self._attributes_sub_state, { @@ -293,9 +353,13 @@ def attributes_message_received(msg: ReceiveMessage) -> None: }, ) + async def _attributes_subscribe_topics(self): + """(Re)Subscribe to topics.""" + await async_subscribe_topics(self.hass, self._attributes_sub_state) + async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - self._attributes_sub_state = await async_unsubscribe_topics( + self._attributes_sub_state = async_unsubscribe_topics( self.hass, self._attributes_sub_state ) @@ -318,6 +382,7 @@ def __init__(self, config: dict) -> None: async def async_added_to_hass(self) -> None: """Subscribe MQTT events.""" await super().async_added_to_hass() + self._availability_prepare_subscribe_topics() await self._availability_subscribe_topics() self.async_on_remove( async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) @@ -328,9 +393,13 @@ async def async_added_to_hass(self) -> None: ) ) - async def availability_discovery_update(self, config: dict): + def availability_prepare_discovery_update(self, config: dict): """Handle updated discovery message.""" self._availability_setup_from_config(config) + self._availability_prepare_subscribe_topics() + + async def availability_discovery_update(self, config: dict): + """Handle updated discovery message.""" await self._availability_subscribe_topics() def _availability_setup_from_config(self, config): @@ -351,10 +420,7 @@ def _availability_setup_from_config(self, config): CONF_AVAILABILITY_TEMPLATE: avail.get(CONF_VALUE_TEMPLATE), } - for ( - topic, # pylint: disable=unused-variable - avail_topic_conf, - ) in self._avail_topics.items(): + for avail_topic_conf in self._avail_topics.values(): avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = MqttValueTemplate( avail_topic_conf[CONF_AVAILABILITY_TEMPLATE], entity=self, @@ -362,7 +428,7 @@ def _availability_setup_from_config(self, config): self._avail_config = config - async def _availability_subscribe_topics(self): + def _availability_prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -394,12 +460,16 @@ def availability_message_received(msg: ReceiveMessage) -> None: for topic in self._avail_topics } - self._availability_sub_state = await async_subscribe_topics( + self._availability_sub_state = async_prepare_subscribe_topics( self.hass, self._availability_sub_state, topics, ) + async def _availability_subscribe_topics(self): + """(Re)Subscribe to topics.""" + await async_subscribe_topics(self.hass, self._availability_sub_state) + @callback def async_mqtt_connect(self): """Update state on connection/disconnection to MQTT broker.""" @@ -408,7 +478,7 @@ def async_mqtt_connect(self): async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - self._availability_sub_state = await async_unsubscribe_topics( + self._availability_sub_state = async_unsubscribe_topics( self.hass, self._availability_sub_state ) @@ -426,23 +496,25 @@ def available(self) -> bool: return self._available_latest -async def cleanup_device_registry(hass, device_id): +async def cleanup_device_registry(hass, device_id, config_entry_id): """Remove device registry entry if there are no remaining entities or triggers.""" # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import device_trigger, tag - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) if ( device_id - and not hass.helpers.entity_registry.async_entries_for_device( + and not er.async_entries_for_device( entity_registry, device_id, include_disabled_entities=False ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) ): - device_registry.async_remove_device(device_id) + device_registry.async_update_device( + device_id, remove_config_entry_id=config_entry_id + ) class MqttDiscoveryUpdate(Entity): @@ -469,13 +541,12 @@ async def _async_remove_state_and_registry_entry(self) -> None: Remove entity from entity registry if it is registered, this also removes the state. If the entity is not in the entity registry, just remove the state. """ - entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) - if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) + entity_registry = er.async_get(self.hass) + if entity_entry := entity_registry.async_get(self.entity_id): entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) + await cleanup_device_registry( + self.hass, entity_entry.device_id, entity_entry.config_entry_id + ) else: await self.async_remove(force_remove=True) @@ -529,7 +600,7 @@ async def async_removed_from_registry(self) -> None: # Clear the discovery topic so the entity is not rediscovered after a restart discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish(self.hass, discovery_topic, "", retain=True) + await async_publish(self.hass, discovery_topic, "", retain=True) @callback def add_to_platform_abort(self) -> None: @@ -599,10 +670,10 @@ def __init__(self, device_config: ConfigType | None, config_entry=None) -> None: self._device_config = device_config self._config_entry = config_entry - async def device_info_discovery_update(self, config: dict): + def device_info_discovery_update(self, config: dict): """Handle updated discovery message.""" self._device_config = config.get(CONF_DEVICE) - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) config_entry_id = self._config_entry.entry_id device_info = self.device_info @@ -655,6 +726,7 @@ def _init_entity_id(self): async def async_added_to_hass(self): """Subscribe mqtt events.""" await super().async_added_to_hass() + self._prepare_subscribe_topics() await self._subscribe_topics() async def discovery_update(self, discovery_payload): @@ -662,15 +734,22 @@ async def discovery_update(self, discovery_payload): config = self.config_schema()(discovery_payload) self._config = config self._setup_from_config(self._config) + + # Prepare MQTT subscriptions + self.attributes_prepare_discovery_update(config) + self.availability_prepare_discovery_update(config) + self.device_info_discovery_update(config) + self._prepare_subscribe_topics() + + # Finalize MQTT subscriptions await self.attributes_discovery_update(config) await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( + self._sub_state = subscription.async_unsubscribe_topics( self.hass, self._sub_state ) await MqttAttributes.async_will_remove_from_hass(self) @@ -678,6 +757,25 @@ async def async_will_remove_from_hass(self): await MqttDiscoveryUpdate.async_will_remove_from_hass(self) debug_info.remove_entity_data(self.hass, self.entity_id) + async def async_publish( + self, + topic: str, + payload: PublishPayloadType, + qos: int = 0, + retain: bool = False, + encoding: str = DEFAULT_ENCODING, + ): + """Publish message to an MQTT topic.""" + log_message(self.hass, self.entity_id, topic, payload, qos, retain) + await async_publish( + self.hass, + topic, + payload, + qos, + retain, + encoding, + ) + @staticmethod @abstractmethod def config_schema(): @@ -686,6 +784,10 @@ def config_schema(): def _setup_from_config(self, config): """(Re)Setup the entity.""" + @abstractmethod + def _prepare_subscribe_topics(self): + """(Re)Subscribe to topics.""" + @abstractmethod async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -719,3 +821,29 @@ def should_poll(self): def unique_id(self): """Return a unique ID.""" return self._unique_id + + +@callback +def async_removed_from_device( + hass: HomeAssistant, event: Event, mqtt_device_id: str, config_entry_id: str +) -> bool: + """Check if the passed event indicates MQTT was removed from a device.""" + device_id = event.data["device_id"] + if event.data["action"] not in ("remove", "update"): + return False + + if device_id != mqtt_device_id: + return False + + if event.data["action"] == "update": + if "config_entries" not in event.data["changes"]: + return False + device_registry = dr.async_get(hass) + if not (device_entry := device_registry.async_get(device_id)): + # The device is already removed, do cleanup when we get "remove" event + return False + if config_entry_id in device_entry.config_entries: + # Not removed from device + return False + + return True diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 0020a020411ee..e13ae4ded84f2 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -23,11 +23,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -36,10 +35,14 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) _LOGGER = logging.getLogger(__name__) @@ -103,8 +106,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT number through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, number.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -162,7 +166,7 @@ def _setup_from_config(self, config): ).async_render_with_possible_json_value, } - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -200,7 +204,7 @@ def message_received(msg): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -213,6 +217,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(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._current_number = last_state.state @@ -253,8 +261,7 @@ async def async_set_value(self, value: float) -> None: self._current_number = current_number self.async_write_ha_state() - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index b12eb2d336ac4..c44ea1dca533e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -12,18 +12,17 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS from .. import mqtt -from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, DOMAIN +from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, async_setup_entry_helper, + async_setup_platform_helper, init_entity_id_from_config, ) @@ -52,8 +51,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT scene through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 810c6126d52b1..f873a32a5de79 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -13,11 +13,10 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttCommandTemplate, MqttValueTemplate, subscription +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TEMPLATE, @@ -26,10 +25,14 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) _LOGGER = logging.getLogger(__name__) @@ -67,8 +70,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT select through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, select.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -128,7 +132,7 @@ def _setup_from_config(self, config): ).async_render_with_possible_json_value, } - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -156,7 +160,7 @@ def message_received(msg): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -169,6 +173,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(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._attr_current_option = last_state.state @@ -179,8 +187,7 @@ async def async_select_option(self, option: str) -> None: self._attr_current_option = option self.async_write_ha_state() - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], payload, self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 13f58a8494ab8..c24535ebd1fae 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -13,8 +13,8 @@ DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, STATE_CLASSES_SCHEMA, + RestoreSensor, SensorDeviceClass, - SensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,20 +30,19 @@ 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.reload import async_setup_reload_service -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import PLATFORMS, MqttValueTemplate, subscription +from . import MqttValueTemplate, subscription from .. import mqtt -from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, DOMAIN +from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, async_setup_entry_helper, + async_setup_platform_helper, ) _LOGGER = logging.getLogger(__name__) @@ -120,8 +119,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT sensors through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -143,7 +143,7 @@ async def _async_setup_entity( async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) -class MqttSensor(MqttEntity, SensorEntity, RestoreEntity): +class MqttSensor(MqttEntity, RestoreSensor): """Representation of a sensor that can be updated using MQTT.""" _entity_id_format = ENTITY_ID_FORMAT @@ -171,6 +171,8 @@ async def async_added_to_hass(self) -> 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()) + is not None # We might have set up a trigger already after subscribing from # super().async_added_to_hass(), then we should not restore state and not self._expiration_trigger @@ -181,7 +183,7 @@ async def async_added_to_hass(self) -> None: _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_state.state + self._state = last_sensor_data.native_value self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at @@ -216,7 +218,7 @@ def _setup_from_config(self, config): self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -306,10 +308,14 @@ def last_reset_message_received(msg): "encoding": self._config[CONF_ENCODING] or None, } - self._sub_state = await subscription.async_subscribe_topics( + 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) + @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py new file mode 100644 index 0000000000000..8619bc799a497 --- /dev/null +++ b/homeassistant/components/mqtt/siren.py @@ -0,0 +1,381 @@ +"""Support for MQTT sirens.""" +from __future__ import annotations + +import copy +import functools +import json +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components import siren +from homeassistant.components.siren import ( + TURN_ON_SCHEMA, + SirenEntity, + process_turn_on_params, +) +from homeassistant.components.siren.const import ( + ATTR_AVAILABLE_TONES, + ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, + SUPPORT_DURATION, + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_OPTIMISTIC, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, +) +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, DiscoveryInfoType + +from . import MqttCommandTemplate, MqttValueTemplate, subscription +from .. import mqtt +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_STATE_VALUE_TEMPLATE, + PAYLOAD_EMPTY_JSON, + PAYLOAD_NONE, +) +from .debug_info import log_messages +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) + +DEFAULT_NAME = "MQTT Siren" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_OPTIMISTIC = False + +ENTITY_ID_FORMAT = siren.DOMAIN + ".{}" + +CONF_AVAILABLE_TONES = "available_tones" +CONF_COMMAND_OFF_TEMPLATE = "command_off_template" +CONF_STATE_ON = "state_on" +CONF_STATE_OFF = "state_off" +CONF_SUPPORT_DURATION = "support_duration" +CONF_SUPPORT_VOLUME_SET = "support_volume_set" + +STATE = "state" + +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_COMMAND_OFF_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_STATE_OFF): cv.string, + vol.Optional(CONF_STATE_ON): cv.string, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SUPPORT_DURATION, default=True): cv.boolean, + vol.Optional(CONF_SUPPORT_VOLUME_SET, default=True): cv.boolean, + }, +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)) + +MQTT_SIREN_ATTRIBUTES_BLOCKED = frozenset( + { + ATTR_AVAILABLE_TONES, + ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, + } +) + +SUPPORTED_BASE = SUPPORT_TURN_OFF | SUPPORT_TURN_ON + +SUPPORTED_ATTRIBUTES = { + ATTR_DURATION: SUPPORT_DURATION, + ATTR_TONE: SUPPORT_TONES, + ATTR_VOLUME_LEVEL: SUPPORT_VOLUME_SET, +} + +_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 siren through configuration.yaml.""" + await async_setup_platform_helper( + hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT siren dynamically through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, siren.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass, async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT siren.""" + async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) + + +class MqttSiren(MqttEntity, SirenEntity): + """Representation of a siren that can be controlled using MQTT.""" + + _entity_id_format = ENTITY_ID_FORMAT + _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED + + def __init__(self, hass, config, config_entry, discovery_data): + """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) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + + state_on = 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) + self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] + + if config[CONF_SUPPORT_DURATION]: + self._supported_features |= SUPPORT_DURATION + self._attr_extra_state_attributes[ATTR_DURATION] = None + + if config.get(CONF_AVAILABLE_TONES): + self._supported_features |= SUPPORT_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 |= SUPPORT_VOLUME_SET + self._attr_extra_state_attributes[ATTR_VOLUME_LEVEL] = None + + 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 + ) + self._command_templates = { + CONF_COMMAND_TEMPLATE: MqttCommandTemplate( + command_template, entity=self + ).async_render + if command_template + else None, + CONF_COMMAND_OFF_TEMPLATE: MqttCommandTemplate( + command_off_template, entity=self + ).async_render + if command_off_template + else None, + } + self._value_template = MqttValueTemplate( + config.get(CONF_STATE_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value + + def _prepare_subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def state_message_received(msg): + """Handle new MQTT state messages.""" + payload = self._value_template(msg.payload) + if not payload or payload == PAYLOAD_EMPTY_JSON: + _LOGGER.debug( + "Ignoring empty payload '%s' after rendering for topic %s", + payload, + msg.topic, + ) + return + json_payload = {} + if payload in [self._state_on, self._state_off, PAYLOAD_NONE]: + json_payload = {STATE: payload} + else: + try: + json_payload = json.loads(payload) + _LOGGER.debug( + "JSON payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + except json.decoder.JSONDecodeError: + _LOGGER.warning( + "No valid (JSON) payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + return + if STATE in json_payload: + if json_payload[STATE] == self._state_on: + self._attr_is_on = True + if json_payload[STATE] == self._state_off: + self._attr_is_on = False + if json_payload[STATE] == PAYLOAD_NONE: + self._attr_is_on = None + del json_payload[STATE] + + if json_payload: + # process attributes + try: + 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", + json_payload, + invalid_siren_parameters, + ) + return + self._update(process_turn_on_params(self, json_payload)) + self.async_write_ha_state() + + if self._config.get(CONF_STATE_TOPIC) is None: + # Force into optimistic mode. + self._optimistic = True + else: + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, + self._sub_state, + { + CONF_STATE_TOPIC: { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": state_message_received, + "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, + } + }, + ) + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def extra_state_attributes(self) -> dict: + """Return the state attributes.""" + mqtt_attributes = super().extra_state_attributes + attributes = ( + copy.deepcopy(mqtt_attributes) if mqtt_attributes is not None else {} + ) + 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, + template: str, + value: Any, + variables: dict[str, Any] | None = None, + ) -> None: + """Publish MQTT payload with optional command template.""" + template_variables = {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: + await self.async_publish( + self._config[topic], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) + + async def async_turn_on(self, **kwargs) -> None: + """Turn the siren on. + + This method is a coroutine. + """ + await self._async_publish( + CONF_COMMAND_TOPIC, + CONF_COMMAND_TEMPLATE, + self._config[CONF_PAYLOAD_ON], + kwargs, + ) + if self._optimistic: + # Optimistically assume that siren has changed state. + _LOGGER.debug("Writing state attributes %s", kwargs) + self._attr_is_on = True + self._update(kwargs) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the siren off. + + This method is a coroutine. + """ + await self._async_publish( + CONF_COMMAND_TOPIC, + CONF_COMMAND_OFF_TEMPLATE, + self._config[CONF_PAYLOAD_OFF], + ) + + if self._optimistic: + # Optimistically assume that siren has changed state. + self._attr_is_on = False + self.async_write_ha_state() + + def _update(self, data: dict[str, Any]) -> 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] diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 6d132b28a98cf..d0af533f2947a 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -1,13 +1,12 @@ """Helper to handle a set of topics to subscribe to.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from typing import Any import attr from homeassistant.core import HomeAssistant -from homeassistant.loader import bind_hass from . import debug_info from .. import mqtt @@ -22,11 +21,12 @@ class EntitySubscription: hass: HomeAssistant = attr.ib() topic: str = attr.ib() message_callback: MessageCallbackType = attr.ib() + subscribe_task: Coroutine | None = attr.ib() unsubscribe_callback: Callable[[], None] | None = attr.ib() qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") - async def resubscribe_if_necessary(self, hass, other): + def resubscribe_if_necessary(self, hass, other): """Re-subscribe to the new topic if necessary.""" if not self._should_resubscribe(other): self.unsubscribe_callback = other.unsubscribe_callback @@ -46,33 +46,41 @@ async def resubscribe_if_necessary(self, hass, other): # Prepare debug data debug_info.add_subscription(self.hass, self.message_callback, self.topic) - self.unsubscribe_callback = await mqtt.async_subscribe( + self.subscribe_task = mqtt.async_subscribe( hass, self.topic, self.message_callback, self.qos, self.encoding ) + async def subscribe(self): + """Subscribe to a topic.""" + if not self.subscribe_task: + return + self.unsubscribe_callback = await self.subscribe_task + def _should_resubscribe(self, other): """Check if we should re-subscribe to the topic using the old state.""" if other is None: return True - return (self.topic, self.qos, self.encoding) != ( + return (self.topic, self.qos, self.encoding,) != ( other.topic, other.qos, other.encoding, ) -@bind_hass -async def async_subscribe_topics( +def async_prepare_subscribe_topics( hass: HomeAssistant, new_state: dict[str, EntitySubscription] | None, topics: dict[str, Any], ) -> dict[str, EntitySubscription]: - """(Re)Subscribe to a set of MQTT topics. + """Prepare (re)subscribe to a set of MQTT topics. State is kept in sub_state and a dictionary mapping from the subscription key to the subscription state. + After this function has been called, async_subscribe_topics must be called to + finalize any new subscriptions. + Please note that the sub state must not be shared between multiple sets of topics. Every call to async_subscribe_topics must always contain _all_ the topics the subscription state should manage. @@ -88,10 +96,11 @@ async def async_subscribe_topics( qos=value.get("qos", DEFAULT_QOS), encoding=value.get("encoding", "utf-8"), hass=hass, + subscribe_task=None, ) # Get the current subscription state current = current_subscriptions.pop(key, None) - await requested.resubscribe_if_necessary(hass, current) + requested.resubscribe_if_necessary(hass, current) new_state[key] = requested # Go through all remaining subscriptions and unsubscribe them @@ -106,9 +115,19 @@ async def async_subscribe_topics( return new_state -@bind_hass -async def async_unsubscribe_topics( +async def async_subscribe_topics( + hass: HomeAssistant, + sub_state: dict[str, EntitySubscription] | None, +) -> None: + """(Re)Subscribe to a set of MQTT topics.""" + if sub_state is None: + return + for sub in sub_state.values(): + await sub.subscribe() + + +def async_unsubscribe_topics( hass: HomeAssistant, sub_state: dict[str, EntitySubscription] | None ) -> dict[str, EntitySubscription]: """Unsubscribe from all MQTT topics managed by async_subscribe_topics.""" - return await async_subscribe_topics(hass, sub_state, {}) + return async_prepare_subscribe_topics(hass, sub_state, {}) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 9feba2c1d253c..1a471ea2ad03e 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -20,11 +20,10 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import PLATFORMS, MqttValueTemplate, subscription +from . import MqttValueTemplate, subscription from .. import mqtt from .const import ( CONF_COMMAND_TOPIC, @@ -32,10 +31,15 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - DOMAIN, + PAYLOAD_NONE, ) from .debug_info import log_messages -from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_setup_entry_helper, + async_setup_platform_helper, +) MQTT_SWITCH_ATTRIBUTES_BLOCKED = frozenset( { @@ -74,8 +78,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT switch through configuration.yaml.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry( @@ -105,7 +110,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" - self._state = False + self._state = None self._state_on = None self._state_off = None @@ -126,13 +131,15 @@ def _setup_from_config(self, config): state_off = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] - self._optimistic = config[CONF_OPTIMISTIC] + self._optimistic = ( + config[CONF_OPTIMISTIC] or config.get(CONF_STATE_TOPIC) is None + ) self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback @@ -144,6 +151,8 @@ def state_message_received(msg): self._state = True elif payload == self._state_off: self._state = False + elif payload == PAYLOAD_NONE: + self._state = None self.async_write_ha_state() @@ -151,7 +160,7 @@ def state_message_received(msg): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -164,11 +173,15 @@ def state_message_received(msg): }, ) + async def _subscribe_topics(self): + """(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): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state @@ -187,8 +200,7 @@ async def async_turn_on(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], self._config[CONF_QOS], @@ -205,8 +217,7 @@ async def async_turn_off(self, **kwargs): This method is a coroutine. """ - await mqtt.async_publish( - self.hass, + await self.async_publish( self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_OFF], self._config[CONF_QOS], diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index b2638f8ac4b0f..a2541c064c023 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import ( @@ -26,6 +27,7 @@ CONF_CONNECTIONS, CONF_IDENTIFIERS, MQTT_ENTITY_DEVICE_INFO_SCHEMA, + async_removed_from_device, async_setup_entry_helper, cleanup_device_registry, device_info_from_config, @@ -61,9 +63,9 @@ async def async_setup_tag(hass, config, config_entry, discovery_data): device_id = None if CONF_DEVICE in config: - await _update_device(hass, config_entry, config) + _update_device(hass, config_entry, config) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( {(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]}, {tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]}, @@ -125,16 +127,18 @@ async def discovery_update(self, payload): if not payload: # Empty payload: Remove tag scanner _LOGGER.info("Removing tag scanner: %s", discovery_hash) - await self.tear_down() + self.tear_down() if self.device_id: - await cleanup_device_registry(self.hass, self.device_id) + await cleanup_device_registry( + self.hass, self.device_id, self._config_entry.entry_id + ) else: # Non-empty payload: Update tag scanner _LOGGER.info("Updating tag scanner: %s", discovery_hash) config = PLATFORM_SCHEMA(payload) self._config = config if self.device_id: - await _update_device(self.hass, self._config_entry, config) + _update_device(self.hass, self._config_entry, config) self._setup_from_config(config) await self.subscribe_topics() @@ -154,7 +158,7 @@ async def setup(self): await self.subscribe_topics() if self.device_id: self._remove_device_updated = self.hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, self.device_removed + EVENT_DEVICE_REGISTRY_UPDATED, self.device_updated ) self._remove_discovery = async_dispatcher_connect( self.hass, @@ -173,9 +177,12 @@ async def tag_scanned(msg): if not tag_id: # No output from template, ignore return - await self.hass.components.tag.async_scan_tag(tag_id, self.device_id) + # 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) - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -186,37 +193,43 @@ async def tag_scanned(msg): } }, ) + await subscription.async_subscribe_topics(self.hass, self._sub_state) - async def device_removed(self, event): - """Handle the removal of a device.""" - device_id = event.data["device_id"] - if event.data["action"] != "remove" or device_id != self.device_id: + async def device_updated(self, event): + """Handle the update or removal of a device.""" + if not async_removed_from_device( + self.hass, event, self.device_id, self._config_entry.entry_id + ): return - await self.tear_down() + # Stop subscribing to discovery updates to not trigger when we clear the + # discovery topic + self.tear_down() - async def tear_down(self): + # Clear the discovery topic so the entity is not rediscovered after a restart + discovery_topic = self.discovery_data[ATTR_DISCOVERY_TOPIC] + mqtt.publish(self.hass, discovery_topic, "", retain=True) + + def tear_down(self): """Cleanup tag scanner.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] - discovery_topic = self.discovery_data[ATTR_DISCOVERY_TOPIC] clear_discovery_hash(self.hass, discovery_hash) if self.device_id: self._remove_device_updated() self._remove_discovery() - mqtt.publish(self.hass, discovery_topic, "", retain=True) - self._sub_state = await subscription.async_unsubscribe_topics( + self._sub_state = subscription.async_unsubscribe_topics( self.hass, self._sub_state ) if self.device_id: self.hass.data[TAGS][self.device_id].pop(discovery_id) -async def _update_device(hass, config_entry, config): +def _update_device(hass, config_entry, config): """Update device registry.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) config_entry_id = config_entry.entry_id device_info = device_info_from_config(config[CONF_DEVICE]) diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index 44f835c57d993..6921604bdee00 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7", + "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" }, "step": { "broker": { "data": { - "discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\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", + "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" }, "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." }, @@ -14,6 +22,7 @@ "data": { "discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2" }, + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf {addon};", "title": "MQTT Broker \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Home Assistant" } } @@ -30,16 +39,47 @@ "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" }, "trigger_type": { + "button_double_press": "\u0394\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \"{subtype}\"", + "button_long_press": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c5\u03bd\u03b5\u03c7\u03ce\u03c2", + "button_long_release": "\"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", + "button_quadruple_press": "\u03a4\u03b5\u03c4\u03c1\u03b1\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \"{subtype}\"", + "button_quintuple_press": "\u03a0\u03b5\u03bd\u03c4\u03b1\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \"{subtype}\"", "button_short_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", - "button_short_release": "\u0391\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"" + "button_short_release": "\u0391\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", + "button_triple_press": "\u03a4\u03c1\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \"{subtype}\"" } }, "options": { + "error": { + "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 birth.", + "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" + }, "step": { "broker": { + "data": { + "broker": "\u039c\u03b5\u03c3\u03af\u03c4\u03b7\u03c2", + "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" + }, + "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" }, "options": { + "data": { + "birth_enable": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 birth", + "birth_payload": "\u03a9\u03c6\u03ad\u03bb\u03b9\u03bc\u03bf \u03c6\u03bf\u03c1\u03c4\u03af\u03bf \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 birth", + "birth_qos": "QoS \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 birth", + "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", + "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", + "will_retain": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will", + "will_topic": "\u0398\u03ad\u03bc\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 will" + }, "description": "\u0391\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 - \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9), \u03c4\u03bf Home Assistant \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c8\u03b5\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03ba\u03b1\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b7\u03bc\u03bf\u03c3\u03b9\u03b5\u03cd\u03bf\u03c5\u03bd \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c3\u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT. \u0395\u03ac\u03bd \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b3\u03af\u03bd\u03bf\u03c5\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1.\nBirth message (\u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2) - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03c4\u03bf Home Assistant (\u03b5\u03c0\u03b1\u03bd\u03b1)\u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7 MQTT.\nWill message - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 will \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03c7\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c4\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bc\u03b5\u03c3\u03af\u03c4\u03b7, \u03c4\u03cc\u03c3\u03bf \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b8\u03b1\u03c1\u03ae\u03c2 (\u03c0.\u03c7. \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant) \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b7 \u03ba\u03b1\u03b8\u03b1\u03c1\u03ae\u03c2 (\u03c0.\u03c7. \u03c3\u03c5\u03bd\u03c4\u03c1\u03b9\u03b2\u03ae \u03c4\u03bf\u03c5 Home Assistant \u03ae \u03b1\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5) \u03b1\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 MQTT" } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 50cac3172ab5b..89a5ce04d978f 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -2,28 +2,28 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de MQTT." + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo se permite una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "No se puede conectar con el agente" + "cannot_connect": "No se puede conectar" }, "step": { "broker": { "data": { - "broker": "Agente", + "broker": "Br\u00f3ker", "discovery": "Habilitar descubrimiento", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Usuario" }, - "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT" + "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT." }, "hassio_confirm": { "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Supervisor {addon}?", - "title": "MQTT Broker a trav\u00e9s del complemento Supervisor" + "description": "\u00bfDesea configurar Home Assistant para conectar con el br\u00f3ker de MQTT proporcionado por el complemento {addon}?", + "title": "Br\u00f3ker MQTT a trav\u00e9s de complemento de Home Assistant" } } }, @@ -52,19 +52,19 @@ "options": { "error": { "bad_birth": "Tema de nacimiento inv\u00e1lido.", - "bad_will": "Tema deseado inv\u00e1lido.", + "bad_will": "Tema de voluntad inv\u00e1lido.", "cannot_connect": "No se pudo conectar" }, "step": { "broker": { "data": { - "broker": "Agente", + "broker": "Br\u00f3ker", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Usuario" }, - "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT.", - "title": "Opciones para el Broker" + "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT.", + "title": "Opciones del br\u00f3ker" }, "options": { "data": { @@ -74,14 +74,14 @@ "birth_retain": "Retenci\u00f3n del mensaje de nacimiento", "birth_topic": "Tema del mensaje de nacimiento", "discovery": "Habilitar descubrimiento", - "will_enable": "Habilitar mensaje de nacimiento", - "will_payload": "Enviar\u00e1 la carga", - "will_qos": "El mensaje usar\u00e1 el QoS", - "will_retain": "Retendr\u00e1 el mensaje", - "will_topic": "Enviar\u00e1 un mensaje al tema" + "will_enable": "Habilitar mensaje de voluntad", + "will_payload": "Carga del mensaje de voluntad", + "will_qos": "QoS del mensaje de voluntad", + "will_retain": "Retenci\u00f3n del mensaje de voluntad", + "will_topic": "Tema del mensaje de voluntad" }, - "description": "Por favor, selecciona las opciones para MQTT.", - "title": "Opciones para MQTT" + "description": "Descubrimiento - Si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publiquen su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe hacerse manualmente.\nMensaje de nacimiento - El mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte al br\u00f3ker MQTT.\nMensaje de voluntad - El mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en el caso de una desconexi\u00f3n limpia (por ejemplo, el cierre de Home Assistant) como en el caso de una desconexi\u00f3n no limpia (por ejemplo, el cierre de Home Assistant o la p\u00e9rdida de su conexi\u00f3n de red).", + "title": "Opciones de MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 0ed8311339f71..6317fdf61d5c2 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -77,8 +77,8 @@ "will_enable": "Abilita il messaggio testamento", "will_payload": "Payload del messaggio testamento", "will_qos": "QoS del messaggio testamento", - "will_retain": "Persistenza del messaggio testamento", - "will_topic": "Argomento 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.", "title": "Opzioni MQTT" diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index ef9fad1444054..526fe072cf790 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao Broker" + "cannot_connect": "Falha ao conectar" }, "step": { "broker": { @@ -21,8 +22,8 @@ "data": { "discovery": "Ativar descoberta" }, - "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento Supervisor {addon}?", - "title": "MQTT Broker via add-on Supervisor" + "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo add-on {addon}?", + "title": "MQTT Broker via add-on" } } }, @@ -36,6 +37,52 @@ "button_6": "Sexto bot\u00e3o", "turn_off": "Desligar", "turn_on": "Ligar" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" clicado duas vezes", + "button_long_press": "\"{subtype}\" continuamente pressionado", + "button_long_release": "\"{subtype}\" lan\u00e7ado ap\u00f3s longa prensa", + "button_quadruple_press": "\"{subtype}\" quadruplicado", + "button_quintuple_press": "\"{subtype}\" quintuplo clicado", + "button_short_press": "\"{subtype}\" pressionado", + "button_short_release": "\"{subtype}\" lan\u00e7ados", + "button_triple_press": "\"{subtype}\" triplo clicado" + } + }, + "options": { + "error": { + "bad_birth": "T\u00f3pico \u00b4Birth message\u00b4 inv\u00e1lido", + "bad_will": "T\u00f3pico \u00b4Will message\u00b4 inv\u00e1lido", + "cannot_connect": "Falha ao conectar" + }, + "step": { + "broker": { + "data": { + "broker": "", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "description": "Insira as informa\u00e7\u00f5es de conex\u00e3o do seu broker MQTT.", + "title": "Op\u00e7\u00f5es do broker" + }, + "options": { + "data": { + "birth_enable": "Ativar \u00b4Birth message\u00b4", + "birth_payload": "Payload \u00b4Birth message\u00b4", + "birth_qos": "QoS \u00b4Birth message\u00b4", + "birth_retain": "Retain \u00b4Birth message\u00b4", + "birth_topic": "T\u00f3pico \u00b4Birth message\u00b4", + "discovery": "Ativar descoberta", + "will_enable": "Ativar `Will message`", + "will_payload": "Payload `Will message`", + "will_qos": "QoS `Will message`", + "will_retain": "Retain `Will message`", + "will_topic": "T\u00f3pico `Will message`" + }, + "description": "Descoberta - Se a descoberta estiver habilitada (recomendado), o Home Assistant descobrir\u00e1 automaticamente dispositivos e entidades que publicam suas configura\u00e7\u00f5es no broker MQTT. Se a descoberta estiver desabilitada, toda a configura\u00e7\u00e3o dever\u00e1 ser feita manualmente.\n\u00b4Birth message\u00b4 - Ser\u00e1 enviada sempre que o Home Assistant (re)conectar-se ao broker MQTT.\n`Will message` - Ser\u00e1 enviada sempre que o Home Assistant perder sua conex\u00e3o com o broker, tanto no caso de uma parada programada (por exemplo, o Home Assistant desligando) quanto no caso de uma parada inesperada (por exemplo, o Home Assistant travando ou perdendo sua conex\u00e3o de rede).", + "title": "Op\u00e7\u00f5es de MQTT" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sk.json b/homeassistant/components/mqtt/translations/sk.json new file mode 100644 index 0000000000000..e01295844ec07 --- /dev/null +++ b/homeassistant/components/mqtt/translations/sk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "broker": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "options": { + "step": { + "broker": { + "data": { + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index b8cbab32b14fd..b684595b1704b 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 9b08ba9aee88c..43d6a5f0b4e0b 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 6ff03a437e541..f64a67820d43e 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -3,11 +3,9 @@ import voluptuous as vol -from homeassistant.components.vacuum import DOMAIN -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.components import vacuum -from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS -from ..mixins import async_setup_entry_helper +from ..mixins import async_setup_entry_helper, async_setup_platform_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( DISCOVERY_SCHEMA_LEGACY, @@ -44,8 +42,9 @@ def validate_mqtt_vacuum(value): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" - await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(hass, async_add_entities, config) + await async_setup_platform_helper( + hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity + ) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -53,7 +52,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, DOMAIN, setup, DISCOVERY_SCHEMA) + await async_setup_entry_helper(hass, vacuum.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 5f85acc75ca6c..087de1086b59c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -240,7 +240,7 @@ def _setup_from_config(self, config): ) } - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: @@ -325,7 +325,7 @@ def message_received(msg): self.async_write_ha_state() topics_list = {topic for topic in self._state_topics.values() if topic} - self._sub_state = await subscription.async_subscribe_topics( + self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, { @@ -339,6 +339,10 @@ def message_received(msg): }, ) + async def _subscribe_topics(self): + """(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.""" @@ -384,8 +388,7 @@ async def async_turn_on(self, **kwargs): if self.supported_features & SUPPORT_TURN_ON == 0: return - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_TURN_ON], self._qos, @@ -400,8 +403,7 @@ async def async_turn_off(self, **kwargs): if self.supported_features & SUPPORT_TURN_OFF == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_TURN_OFF], self._qos, @@ -416,8 +418,7 @@ async def async_stop(self, **kwargs): if self.supported_features & SUPPORT_STOP == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_STOP], self._qos, @@ -432,8 +433,7 @@ async def async_clean_spot(self, **kwargs): if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_CLEAN_SPOT], self._qos, @@ -448,8 +448,7 @@ async def async_locate(self, **kwargs): if self.supported_features & SUPPORT_LOCATE == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_LOCATE], self._qos, @@ -464,8 +463,7 @@ async def async_start_pause(self, **kwargs): if self.supported_features & SUPPORT_PAUSE == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_START_PAUSE], self._qos, @@ -480,8 +478,7 @@ async def async_return_to_base(self, **kwargs): if self.supported_features & SUPPORT_RETURN_HOME == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], self._qos, @@ -498,8 +495,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): ) or fan_speed not in self._fan_speed_list: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._set_fan_speed_topic, fan_speed, self._qos, @@ -519,8 +515,7 @@ async def async_send_command(self, command, params=None, **kwargs): message = json.dumps(message) else: message = command - await mqtt.async_publish( - self.hass, + await self.async_publish( self._send_command_topic, message, self._qos, diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 872f4d62765a4..e5c138c96ff16 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -197,7 +197,7 @@ def _setup_from_config(self, config): ) } - async def _subscribe_topics(self): + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -206,8 +206,12 @@ async def _subscribe_topics(self): def state_message_received(msg): """Handle state MQTT message.""" payload = json.loads(msg.payload) - if STATE in payload and payload[STATE] in POSSIBLE_STATES: - self._state = POSSIBLE_STATES[payload[STATE]] + if STATE in payload and ( + payload[STATE] in POSSIBLE_STATES or payload[STATE] is None + ): + self._state = ( + POSSIBLE_STATES[payload[STATE]] if payload[STATE] else None + ) del payload[STATE] self._state_attrs.update(payload) self.async_write_ha_state() @@ -219,10 +223,14 @@ def state_message_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._sub_state = await subscription.async_subscribe_topics( + 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 state(self): """Return state of vacuum.""" @@ -252,8 +260,7 @@ async def async_start(self): """Start the vacuum.""" if self.supported_features & SUPPORT_START == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_START], self._config[CONF_QOS], @@ -265,8 +272,7 @@ async def async_pause(self): """Pause the vacuum.""" if self.supported_features & SUPPORT_PAUSE == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_PAUSE], self._config[CONF_QOS], @@ -278,8 +284,7 @@ async def async_stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_STOP], self._config[CONF_QOS], @@ -293,8 +298,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): fan_speed not in self._fan_speed_list ): return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._set_fan_speed_topic, fan_speed, self._config[CONF_QOS], @@ -306,8 +310,7 @@ async def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_RETURN_TO_BASE], self._config[CONF_QOS], @@ -319,8 +322,7 @@ async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_CLEAN_SPOT], self._config[CONF_QOS], @@ -332,8 +334,7 @@ async def async_locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return None - await mqtt.async_publish( - self.hass, + await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_LOCATE], self._config[CONF_QOS], @@ -351,8 +352,7 @@ async def async_send_command(self, command, params=None, **kwargs): message = json.dumps(message) else: message = command - await mqtt.async_publish( - self.hass, + await self.async_publish( self._send_command_topic, message, self._config[CONF_QOS], diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json index 3024bfb310ba3..75691e5fc26ef 100644 --- a/homeassistant/components/msteams/manifest.json +++ b/homeassistant/components/msteams/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/msteams", "requirements": ["pymsteams==0.1.12"], "codeowners": ["@peroyvind"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pymsteams"] } diff --git a/homeassistant/components/mullvad/translations/pt-BR.json b/homeassistant/components/mullvad/translations/pt-BR.json new file mode 100644 index 0000000000000..341389ea117d7 --- /dev/null +++ b/homeassistant/components/mullvad/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "description": "Configurar a integra\u00e7\u00e3o do Mullvad VPN?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/manifest.json b/homeassistant/components/mutesync/manifest.json index 74e6d89d9f854..1498c6955059c 100644 --- a/homeassistant/components/mutesync/manifest.json +++ b/homeassistant/components/mutesync/manifest.json @@ -7,5 +7,6 @@ "iot_class": "local_polling", "codeowners": [ "@currentoor" - ] + ], + "loggers": ["mutesync"] } diff --git a/homeassistant/components/mutesync/translations/el.json b/homeassistant/components/mutesync/translations/el.json index 0edaee152acd9..8d54f81a6e4df 100644 --- a/homeassistant/components/mutesync/translations/el.json +++ b/homeassistant/components/mutesync/translations/el.json @@ -1,7 +1,16 @@ { "config": { "error": { - "invalid_auth": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf m\u00fctesync \u03a0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9\u03c2 > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf m\u00fctesync \u03a0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9\u03c2 > \u0388\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": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/id.json b/homeassistant/components/mutesync/translations/id.json index 31da1a72f64c2..e67dddbb0b4c9 100644 --- a/homeassistant/components/mutesync/translations/id.json +++ b/homeassistant/components/mutesync/translations/id.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", - "invalid_auth": "Aktifkan autentikasi di m\u00fctesync: Preferensi > Autentikasi", + "invalid_auth": "Aktifkan autentikasi di m\u00fctesync Preferensi > Autentikasi", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/mutesync/translations/pt-BR.json b/homeassistant/components/mutesync/translations/pt-BR.json new file mode 100644 index 0000000000000..fe08f78aa736f --- /dev/null +++ b/homeassistant/components/mutesync/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Habilitar autentica\u00e7\u00e3o em Prefer\u00eancias m\u00fctesync > Autentica\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index 90c4b5a9ec08d..0abb52a166611 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": ["PyMVGLive==1.1.4"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["MVGLive"] } diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 21fc51fa9eeed..da5d4763775be 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": ["mycroftapi==2.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["mycroftapi"] } diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index c8e9c29e4e7f3..0506e589d54b8 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -9,5 +9,6 @@ "models": ["819LMB", "MYQ"] }, "iot_class": "cloud_polling", - "dhcp": [{ "macaddress": "645299*" }] + "dhcp": [{ "macaddress": "645299*" }], + "loggers": ["pkce", "pymyq"] } diff --git a/homeassistant/components/myq/translations/el.json b/homeassistant/components/myq/translations/el.json index db9aadc0b60a3..25805020ed85b 100644 --- a/homeassistant/components/myq/translations/el.json +++ b/homeassistant/components/myq/translations/el.json @@ -1,9 +1,28 @@ { "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" + }, + "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": { "reauth_confirm": { + "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 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 MyQ" + }, + "user": { + "data": { + "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" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 MyQ" } } } diff --git a/homeassistant/components/myq/translations/it.json b/homeassistant/components/myq/translations/it.json index 78747578d0eeb..1d8befd56c961 100644 --- a/homeassistant/components/myq/translations/it.json +++ b/homeassistant/components/myq/translations/it.json @@ -22,7 +22,7 @@ "password": "Password", "username": "Nome utente" }, - "title": "Connettersi al gateway MyQ" + "title": "Connettiti al gateway MyQ" } } } diff --git a/homeassistant/components/myq/translations/pt-BR.json b/homeassistant/components/myq/translations/pt-BR.json index 932b4b8a72e0a..75d340c7571ef 100644 --- a/homeassistant/components/myq/translations/pt-BR.json +++ b/homeassistant/components/myq/translations/pt-BR.json @@ -1,10 +1,28 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} n\u00e3o \u00e9 mais v\u00e1lida.", + "title": "Reautentique sua conta MyQ" + }, "user": { "data": { + "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "title": "Conecte-se ao Gateway MyQ" } } } diff --git a/homeassistant/components/myq/translations/sk.json b/homeassistant/components/myq/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/myq/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 0bf4bb80a18ef..4d3c3046a89ae 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import CONF_OPTIMISTIC, Platform from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType @@ -264,6 +265,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a MySensors config entry from a device.""" + gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][ + config_entry.entry_id + ] + device_id = next( + device_id for domain, device_id in device_entry.identifiers if domain == DOMAIN + ) + node_id = int(device_id.partition("-")[2]) + gateway.sensors.pop(node_id, None) + gateway.tasks.persistence.need_save = True + + return True + + @callback def setup_mysensors_platform( hass: HomeAssistant, diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 0ced152075807..b1e562e878c96 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -65,10 +65,6 @@ def dev_id(self) -> DevId: """ return self.gateway_id, self.node_id, self.child_id, self.value_type - @property - def _logger(self) -> logging.Logger: - return logging.getLogger(f"{__name__}.{self.name}") - async def async_will_remove_from_hass(self) -> None: """Remove this entity from home assistant.""" for platform in PLATFORM_TYPES: @@ -77,9 +73,7 @@ async def async_will_remove_from_hass(self) -> None: platform_dict = self.hass.data[DOMAIN][platform_str] if self.dev_id in platform_dict: del platform_dict[self.dev_id] - self._logger.debug( - "deleted %s from platform %s", self.dev_id, platform - ) + _LOGGER.debug("Deleted %s from platform %s", self.dev_id, platform) @property def _node(self) -> Sensor: diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index b167c8c58defc..be0381ab74ed4 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -3,7 +3,7 @@ import asyncio from collections import defaultdict -from collections.abc import Callable, Coroutine +from collections.abc import Callable import logging import socket import sys @@ -337,9 +337,7 @@ def mysensors_callback(msg: Message) -> None: _LOGGER.debug("Node update: node %s child %s", msg.node_id, msg.child_id) msg_type = msg.gateway.const.MessageType(msg.type) - msg_handler: Callable[ - [HomeAssistant, GatewayId, Message], Coroutine[Any, Any, None] - ] | None = HANDLERS.get(msg_type.name) + msg_handler = HANDLERS.get(msg_type.name) if msg_handler is None: return diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 4d61a2812aecb..57ff12fc6f019 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -1,6 +1,9 @@ """Handle MySensors messages.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from typing import Any + from mysensors import Message from homeassistant.const import Platform @@ -12,7 +15,9 @@ from .device import get_mysensors_devices from .helpers import discover_mysensors_platform, validate_set_msg -HANDLERS = decorator.Registry() +HANDLERS: decorator.Registry[ + str, Callable[[HomeAssistant, GatewayId, Message], Coroutine[Any, Any, None]] +] = decorator.Registry() @HANDLERS.register("set") diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 5b6682393b15f..a5f6711173893 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -31,7 +31,9 @@ ) _LOGGER = logging.getLogger(__name__) -SCHEMAS = Registry() +SCHEMAS: Registry[ + tuple[str, str], Callable[[BaseAsyncGateway, ChildSensor, ValueType], vol.Schema] +] = Registry() @callback diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index 6e7a4f9cdedd2..dafdd7c86bcd6 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["mysensors"] } diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index ba550fbbe2adb..17bf83158b7ac 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -1,8 +1,11 @@ { "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", "duplicate_persistence_file": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistence \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7", "duplicate_topic": "\u03a4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7", + "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_device": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", "invalid_ip": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "invalid_persistence_file": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistance", @@ -13,24 +16,34 @@ "invalid_version": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors", "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" + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { + "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", "duplicate_persistence_file": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistence \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7", "duplicate_topic": "\u03a4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7", + "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_device": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", "invalid_ip": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "invalid_persistence_file": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistance", + "invalid_port": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2", + "invalid_publish_topic": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03af\u03b5\u03c5\u03c3\u03b7\u03c2", + "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" + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "gw_mqtt": { "data": { "persistence_file": "\u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistence (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", + "retain": "mqtt retain", "topic_in_prefix": "\u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 (topic_in_prefix)", "topic_out_prefix": "\u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03be\u03cc\u03b4\u03bf\u03c5 (topic_out_prefix)", "version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors" @@ -49,7 +62,9 @@ "gw_tcp": { "data": { "device": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2", - "persistence_file": "\u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistence (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)" + "persistence_file": "\u03b1\u03c1\u03c7\u03b5\u03af\u03bf persistence (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", + "tcp_port": "\u03b8\u03cd\u03c1\u03b1", + "version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors" }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2 Ethernet" }, diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 4bb5f5cfd1528..411501db49d0e 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -43,12 +43,12 @@ "gw_mqtt": { "data": { "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", - "retain": "retener mqtt", + "retain": "retenci\u00f3n mqtt", "topic_in_prefix": "prefijo para los temas de entrada (topic_in_prefix)", "topic_out_prefix": "prefijo para los temas de salida (topic_out_prefix)", "version": "Versi\u00f3n de MySensors" }, - "description": "Configuraci\u00f3n del gateway MQTT" + "description": "Configuraci\u00f3n de la puerta de enlace MQTT" }, "gw_serial": { "data": { diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 65638dad15eaf..9583735250240 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -43,7 +43,7 @@ "gw_mqtt": { "data": { "persistence_file": "file di persistenza (lascia vuoto per generare automaticamente)", - "retain": "mqtt conserva", + "retain": "Persistenza mqtt", "topic_in_prefix": "prefisso per argomenti di input (topic_in_prefix)", "topic_out_prefix": "prefisso per argomenti di output (topic_out_prefix)", "version": "Versione MySensors" diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json new file mode 100644 index 0000000000000..468acf7058897 --- /dev/null +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -0,0 +1,80 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "duplicate_persistence_file": "Arquivo de persist\u00eancia j\u00e1 em uso", + "duplicate_topic": "T\u00f3pico j\u00e1 em uso", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_device": "Dispositivo inv\u00e1lido", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_persistence_file": "Arquivo de persist\u00eancia inv\u00e1lido", + "invalid_port": "N\u00famero de porta inv\u00e1lido", + "invalid_publish_topic": "T\u00f3pico de publica\u00e7\u00e3o inv\u00e1lido", + "invalid_serial": "Porta serial inv\u00e1lida", + "invalid_subscribe_topic": "T\u00f3pico de inscri\u00e7\u00e3o inv\u00e1lido", + "invalid_version": "Vers\u00e3o MySensors inv\u00e1lida", + "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", + "unknown": "Erro inesperado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "duplicate_persistence_file": "Arquivo de persist\u00eancia j\u00e1 em uso", + "duplicate_topic": "T\u00f3pico j\u00e1 em uso", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_device": "Dispositivo inv\u00e1lido", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_persistence_file": "Arquivo de persist\u00eancia inv\u00e1lido", + "invalid_port": "N\u00famero da porta inv\u00e1lida", + "invalid_publish_topic": "T\u00f3pico de publica\u00e7\u00e3o inv\u00e1lido", + "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", + "unknown": "Erro inesperado" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "arquivo de persist\u00eancia (deixe em branco para gerar automaticamente)", + "retain": "mqtt retain", + "topic_in_prefix": "prefixo para t\u00f3picos de entrada (topic_in_prefix)", + "topic_out_prefix": "prefixo para t\u00f3picos de sa\u00edda (topic_out_prefix)", + "version": "Vers\u00e3o MySensors" + }, + "description": "Configura\u00e7\u00e3o do gateway MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "taxa de transmiss\u00e3o", + "device": "Porta serial", + "persistence_file": "arquivo de persist\u00eancia (deixe em branco para gerar automaticamente)", + "version": "Vers\u00e3o MySensors" + }, + "description": "Configura\u00e7\u00e3o do gateway serial" + }, + "gw_tcp": { + "data": { + "device": "Endere\u00e7o IP do gateway", + "persistence_file": "arquivo de persist\u00eancia (deixe em branco para gerar automaticamente)", + "tcp_port": "porta", + "version": "Vers\u00e3o MySensors" + }, + "description": "Configura\u00e7\u00e3o do gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tipo de gateway" + }, + "description": "Escolha o m\u00e9todo de conex\u00e3o com o gateway" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sk.json b/homeassistant/components/mysensors/translations/sk.json new file mode 100644 index 0000000000000..2c3ed1dd93049 --- /dev/null +++ b/homeassistant/components/mysensors/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 5becef7fff26a..ef13ea4d8bf62 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -5,5 +5,6 @@ "requirements": ["python-mystrom==1.1.2"], "dependencies": ["http"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymystrom"] } diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index 50841f21f3a6e..3b022c1e43d3e 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": ["mbddns==0.1.2"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["mbddns"] } diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 12c1f84aa3765..1cf66c9d43852 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": ["nad_receiver==0.3.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["nad_receiver"] } diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 68d5fb5074613..d8cda2f16c7fb 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -16,5 +16,6 @@ ], "config_flow": true, "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["nettigo_air_monitor"] } diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json new file mode 100644 index 0000000000000..72df4a968182f --- /dev/null +++ b/homeassistant/components/nam/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/el.json b/homeassistant/components/nam/translations/el.json index d3694cacab863..36896d9fa2443 100644 --- a/homeassistant/components/nam/translations/el.json +++ b/homeassistant/components/nam/translations/el.json @@ -1,15 +1,39 @@ { "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", "device_unsupported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", + "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_unsuccessful": "\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 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac." }, + "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" + }, "flow_title": "{host}", "step": { "confirm_discovery": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Nettigo Air Monitor \u03c3\u03c4\u03bf {host};" }, + "credentials": { + "data": { + "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": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2." + }, + "reauth_confirm": { + "data": { + "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": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c9\u03c3\u03c4\u03cc \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae: {host}" + }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Nettigo Air Monitor." } } diff --git a/homeassistant/components/nam/translations/nb.json b/homeassistant/components/nam/translations/nb.json new file mode 100644 index 0000000000000..5d04c17f9326f --- /dev/null +++ b/homeassistant/components/nam/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "username": "Brukernavn" + } + }, + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pt-BR.json b/homeassistant/components/nam/translations/pt-BR.json new file mode 100644 index 0000000000000..270e080701c51 --- /dev/null +++ b/homeassistant/components/nam/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "device_unsupported": "O dispositivo n\u00e3o \u00e9 compat\u00edvel.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_unsuccessful": "A reautentica\u00e7\u00e3o falhou. Remova a integra\u00e7\u00e3o e configure-a novamente." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{host}", + "step": { + "confirm_discovery": { + "description": "Deseja configurar o Nettigo Air Monitor em {host} ?" + }, + "credentials": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Por favor, digite o nome de usu\u00e1rio e senha." + }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Insira o nome de usu\u00e1rio e a senha corretos para o host: {host}" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure a integra\u00e7\u00e3o do Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sk.json b/homeassistant/components/nam/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/nam/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 4560f34041659..9e9cf1d6ca4b0 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -3,17 +3,35 @@ import asyncio from dataclasses import dataclass - -from aionanoleaf import EffectsEvent, InvalidToken, Nanoleaf, StateEvent, Unavailable +from datetime import timedelta +import logging + +from aionanoleaf import ( + EffectsEvent, + InvalidToken, + Nanoleaf, + StateEvent, + TouchEvent, + Unavailable, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_HOST, + CONF_TOKEN, + CONF_TYPE, + Platform, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS -from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BUTTON, Platform.LIGHT] @@ -23,6 +41,7 @@ class NanoleafEntryData: """Class for sharing data within the Nanoleaf integration.""" device: Nanoleaf + coordinator: DataUpdateCoordinator event_listener: asyncio.Task @@ -31,26 +50,59 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nanoleaf = Nanoleaf( async_get_clientsession(hass), entry.data[CONF_HOST], entry.data[CONF_TOKEN] ) - try: - await nanoleaf.get_info() - except Unavailable as err: - raise ConfigEntryNotReady from err - except InvalidToken as err: - raise ConfigEntryAuthFailed from err - - async def _callback_update_light_state(event: StateEvent | EffectsEvent) -> None: + + async def async_get_state() -> None: + """Get the state of the device.""" + try: + await nanoleaf.get_info() + except Unavailable as err: + raise UpdateFailed from err + except InvalidToken as err: + raise ConfigEntryAuthFailed from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=entry.title, + update_interval=timedelta(minutes=1), + update_method=async_get_state, + ) + + await coordinator.async_config_entry_first_refresh() + + async def light_event_callback(event: StateEvent | EffectsEvent) -> None: """Receive state and effect event.""" - async_dispatcher_send(hass, f"{DOMAIN}_update_light_{nanoleaf.serial_no}") + coordinator.async_set_updated_data(None) + + if supports_touch := nanoleaf.model in TOUCH_MODELS: + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, nanoleaf.serial_no)}, + ) + + async def touch_event_callback(event: TouchEvent) -> None: + """Receive touch event.""" + gesture_type = TOUCH_GESTURE_TRIGGER_MAP.get(event.gesture_id) + if gesture_type is None: + _LOGGER.debug("Received unknown touch gesture ID %s", event.gesture_id) + return + _LOGGER.warning("Received touch gesture %s", gesture_type) + hass.bus.async_fire( + NANOLEAF_EVENT, + {CONF_DEVICE_ID: device_entry.id, CONF_TYPE: gesture_type}, + ) event_listener = asyncio.create_task( nanoleaf.listen_events( - state_callback=_callback_update_light_state, - effects_callback=_callback_update_light_state, + state_callback=light_event_callback, + effects_callback=light_event_callback, + touch_callback=touch_event_callback if supports_touch else None, ) ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = NanoleafEntryData( - nanoleaf, event_listener + nanoleaf, coordinator, event_listener ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index 2d9388196c17a..5297e98c88ef3 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NanoleafEntryData from .const import DOMAIN @@ -18,15 +19,17 @@ async def async_setup_entry( ) -> None: """Set up the Nanoleaf button.""" entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafIdentifyButton(entry_data.device)]) + async_add_entities( + [NanoleafIdentifyButton(entry_data.device, entry_data.coordinator)] + ) class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): """Representation of a Nanoleaf identify button.""" - def __init__(self, nanoleaf: Nanoleaf) -> None: + def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: """Initialize the Nanoleaf button.""" - super().__init__(nanoleaf) + super().__init__(nanoleaf, coordinator) self._attr_unique_id = f"{nanoleaf.serial_no}_identify" self._attr_name = f"Identify {nanoleaf.name}" self._attr_icon = "mdi:magnify" diff --git a/homeassistant/components/nanoleaf/const.py b/homeassistant/components/nanoleaf/const.py index 505af8ce69d1d..7e246209733ab 100644 --- a/homeassistant/components/nanoleaf/const.py +++ b/homeassistant/components/nanoleaf/const.py @@ -1,3 +1,14 @@ """Constants for Nanoleaf integration.""" DOMAIN = "nanoleaf" + +NANOLEAF_EVENT = f"{DOMAIN}_event" + +TOUCH_MODELS = {"NL29", "NL42", "NL52"} + +TOUCH_GESTURE_TRIGGER_MAP = { + 2: "swipe_up", + 3: "swipe_down", + 4: "swipe_left", + 5: "swipe_right", +} diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py new file mode 100644 index 0000000000000..311d12506ba7d --- /dev/null +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -0,0 +1,79 @@ +"""Provides device triggers for Nanoleaf.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import DeviceNotFound +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS + +TRIGGER_TYPES = TOUCH_GESTURE_TRIGGER_MAP.values() + +TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List device triggers for Nanoleaf devices.""" + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(device_id) + if device_entry is None: + raise DeviceNotFound(f"Device ID {device_id} is not valid") + if device_entry.model not in TOUCH_MODELS: + return [] + return [ + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: trigger_type, + } + for trigger_type in TRIGGER_TYPES + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: NANOLEAF_EVENT, + event_trigger.CONF_EVENT_DATA: { + CONF_TYPE: config[CONF_TYPE], + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + }, + } + ) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index c6efed9178787..8df9e243d712b 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -2,16 +2,21 @@ from aionanoleaf import Nanoleaf -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN -class NanoleafEntity(Entity): +class NanoleafEntity(CoordinatorEntity): """Representation of a Nanoleaf entity.""" - def __init__(self, nanoleaf: Nanoleaf) -> None: + def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: """Initialize an Nanoleaf entity.""" + super().__init__(coordinator) self._nanoleaf = nanoleaf self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, nanoleaf.serial_no)}, diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index b4eb89ac8c75c..ed3476c4576ac 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,12 +1,11 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations -from datetime import timedelta import logging import math from typing import Any -from aionanoleaf import Nanoleaf, Unavailable +from aionanoleaf import Nanoleaf import voluptuous as vol from homeassistant.components.light import ( @@ -25,11 +24,11 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, @@ -52,8 +51,6 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - async def async_setup_platform( hass: HomeAssistant, @@ -82,15 +79,15 @@ async def async_setup_entry( ) -> None: """Set up the Nanoleaf light.""" entry_data: NanoleafEntryData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NanoleafLight(entry_data.device)]) + async_add_entities([NanoleafLight(entry_data.device, entry_data.coordinator)]) class NanoleafLight(NanoleafEntity, LightEntity): """Representation of a Nanoleaf Light.""" - def __init__(self, nanoleaf: Nanoleaf) -> None: + def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: """Initialize the Nanoleaf light.""" - super().__init__(nanoleaf) + super().__init__(nanoleaf, coordinator) self._attr_unique_id = nanoleaf.serial_no self._attr_name = nanoleaf.name self._attr_min_mireds = math.ceil(1000000 / nanoleaf.color_temperature_max) @@ -156,11 +153,17 @@ async def async_turn_on(self, **kwargs: Any) -> None: effect = kwargs.get(ATTR_EFFECT) transition = kwargs.get(ATTR_TRANSITION) - if hs_color: + if effect: + if effect not in self.effect_list: + raise ValueError( + f"Attempting to apply effect not in the effect list: '{effect}'" + ) + await self._nanoleaf.set_effect(effect) + elif hs_color: hue, saturation = hs_color await self._nanoleaf.set_hue(int(hue)) await self._nanoleaf.set_saturation(int(saturation)) - if color_temp_mired: + elif color_temp_mired: await self._nanoleaf.set_color_temperature( mired_to_kelvin(color_temp_mired) ) @@ -175,46 +178,8 @@ async def async_turn_on(self, **kwargs: Any) -> None: await self._nanoleaf.turn_on() if brightness: await self._nanoleaf.set_brightness(int(brightness / 2.55)) - if effect: - if effect not in self.effect_list: - raise ValueError( - f"Attempting to apply effect not in the effect list: '{effect}'" - ) - await self._nanoleaf.set_effect(effect) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" transition: float | None = kwargs.get(ATTR_TRANSITION) await self._nanoleaf.turn_off(None if transition is None else int(transition)) - - async def async_update(self) -> None: - """Fetch new state data for this light.""" - try: - await self._nanoleaf.get_info() - except Unavailable: - if self.available: - _LOGGER.warning("Could not connect to %s", self.name) - self._attr_available = False - return - if not self.available: - _LOGGER.info("Fetching %s data recovered", self.name) - self._attr_available = True - - @callback - def async_handle_update(self) -> None: - """Handle state update.""" - self.async_write_ha_state() - if not self.available: - _LOGGER.info("Connection to %s recovered", self.name) - self._attr_available = True - - async def async_added_to_hass(self) -> None: - """Handle entity being added to Home Assistant.""" - await super().async_added_to_hass() - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{DOMAIN}_update_light_{self._nanoleaf.serial_no}", - self.async_handle_update, - ) - ) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 3550b56d3520d..8d7d76dc3dbbd 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,11 +3,11 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.1.1"], + "requirements": ["aionanoleaf==0.2.0"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ - "NL*" + "NL29", "NL42", "NL47", "NL48", "NL52", "NL59" ] }, "ssdp": [ @@ -25,5 +25,6 @@ } ], "codeowners": ["@milanmeu"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aionanoleaf"] } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/strings.json b/homeassistant/components/nanoleaf/strings.json index 96fcfd2622ade..80eb2ded7d0e8 100644 --- a/homeassistant/components/nanoleaf/strings.json +++ b/homeassistant/components/nanoleaf/strings.json @@ -24,5 +24,13 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "device_automation": { + "trigger_type": { + "swipe_up": "Swipe Up", + "swipe_down": "Swipe Down", + "swipe_left": "Swipe Left", + "swipe_right": "Swipe Right" + } } } diff --git a/homeassistant/components/nanoleaf/translations/el.json b/homeassistant/components/nanoleaf/translations/el.json index 5112f61ef9ffd..bf3e406a86172 100644 --- a/homeassistant/components/nanoleaf/translations/el.json +++ b/homeassistant/components/nanoleaf/translations/el.json @@ -4,6 +4,7 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_token": "\u0386\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\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\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03bb\u03ac\u03b8\u03bf\u03c2" }, "error": { @@ -16,6 +17,11 @@ "link": { "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf Nanoleaf \u03b3\u03b9\u03b1 5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03bf\u03c5\u03bd \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03bf\u03c5\u03bd \u03bf\u03b9 \u03bb\u03c5\u03c7\u03bd\u03af\u03b5\u03c2 LED \u03c4\u03bf\u03c5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd \u03ba\u03b1\u03b9, \u03c3\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 **SUBMIT** \u03bc\u03ad\u03c3\u03b1 \u03c3\u03b5 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1.", "title": "\u0394\u03b9\u03ac\u03b6\u03b5\u03c5\u03be\u03b7 Nanoleaf" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } } } } diff --git a/homeassistant/components/nanoleaf/translations/en.json b/homeassistant/components/nanoleaf/translations/en.json index 7696f056aa381..7064c617b0ec3 100644 --- a/homeassistant/components/nanoleaf/translations/en.json +++ b/homeassistant/components/nanoleaf/translations/en.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Swipe Down", + "swipe_left": "Swipe Left", + "swipe_right": "Swipe Right", + "swipe_up": "Swipe Up" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pt-BR.json b/homeassistant/components/nanoleaf/translations/pt-BR.json new file mode 100644 index 0000000000000..3946a79471125 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_token": "Token de acesso inv\u00e1lido", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "not_allowing_new_tokens": "Nanoleaf n\u00e3o est\u00e1 permitindo novos tokens, siga as instru\u00e7\u00f5es acima.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "link": { + "description": "Pressione e segure o bot\u00e3o liga/desliga em seu Nanoleaf por 5 segundos at\u00e9 que os LEDs do bot\u00e3o comecem a piscar e clique em **ENVIAR** dentro de 30 segundos.", + "title": "Link Nanoleaf" + }, + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/sk.json b/homeassistant/components/nanoleaf/translations/sk.json new file mode 100644 index 0000000000000..c2f015fe339a0 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/sv.json b/homeassistant/components/nanoleaf/translations/sv.json new file mode 100644 index 0000000000000..4ca6ad5c3de6d --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 1c65ebebdcc59..b183548222dd7 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pybotvac==0.0.23"], "codeowners": ["@dshokouhi", "@Santobert"], "dependencies": ["http"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pybotvac"] } diff --git a/homeassistant/components/neato/translations/el.json b/homeassistant/components/neato/translations/el.json new file mode 100644 index 0000000000000..5e57ddd8b8b5e --- /dev/null +++ b/homeassistant/components/neato/translations/el.json @@ -0,0 +1,23 @@ +{ + "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", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "title": "\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;" + } + } + }, + "title": "Neato Botvac" +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt-BR.json b/homeassistant/components/neato/translations/pt-BR.json new file mode 100644 index 0000000000000..684f7cdff2212 --- /dev/null +++ b/homeassistant/components/neato/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Neato Botvac" +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sk.json b/homeassistant/components/neato/translations/sk.json new file mode 100644 index 0000000000000..520a3afd6d921 --- /dev/null +++ b/homeassistant/components/neato/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 57c89e52ee86d..4aa01428d271a 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": ["nessclient==0.9.15"], "codeowners": ["@nickw444"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["nessclient"] } diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 85513378ed7cd..2a5f3850fe44f 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -158,6 +158,8 @@ async def async_handle_event(self, event_message: EventMessage) -> None: "timestamp": event_message.timestamp, "nest_event_id": image_event.event_token, } + if image_event.zones: + message["zones"] = image_event.zones self._hass.bus.async_fire(NEST_EVENT, message) @@ -354,8 +356,7 @@ async def load_media(self, nest_device: Device, event_token: str) -> Media | Non async def handle_media(self, media: Media) -> web.StreamResponse: """Start a GET request.""" contents = media.contents - content_type = media.content_type - if content_type == "image/jpeg": + if (content_type := media.content_type) == "image/jpeg": image = Image(media.event_image_type.content_type, contents) contents = img_util.scale_jpeg_camera_image( image, THUMBNAIL_SIZE_PX, THUMBNAIL_SIZE_PX diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 858561dd3eadc..0def2e493c2f9 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,24 +4,21 @@ import asyncio from collections.abc import Callable import datetime +import functools import logging from pathlib import Path from google_nest_sdm.camera_traits import ( - CameraEventImageTrait, CameraImageTrait, CameraLiveStreamTrait, RtspStream, StreamingProtocol, ) from google_nest_sdm.device import Device -from google_nest_sdm.event_media import EventMedia from google_nest_sdm.exceptions import ApiException -from haffmpeg.tools import IMAGE_JPEG from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera.const import STREAM_TYPE_WEB_RTC -from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady @@ -76,7 +73,6 @@ def __init__(self, device: Device) -> None: self._create_stream_url_lock = asyncio.Lock() self._stream_refresh_unsub: Callable[[], None] | None = None self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits - self._placeholder_image: bytes | None = None @property def should_poll(self) -> bool: @@ -216,30 +212,18 @@ async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - if CameraEventImageTrait.NAME in self._device.traits: - # Returns the snapshot of the last event for ~30 seconds after the event - event_media: EventMedia | None = None - try: - event_media = ( - await self._device.event_media_manager.get_active_event_media() - ) - except ApiException as err: - _LOGGER.debug("Failure while getting image for event: %s", err) - if event_media: - return event_media.media.contents - # Fetch still image from the live stream - stream_url = await self.stream_source() - if not stream_url: - if self.frontend_stream_type != STREAM_TYPE_WEB_RTC: - return None - # Nest Web RTC cams only have image previews for events, and not - # for "now" by design to save batter, and need a placeholder. - if not self._placeholder_image: - self._placeholder_image = await self.hass.async_add_executor_job( - PLACEHOLDER.read_bytes - ) - return self._placeholder_image - return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) + # Use the thumbnail from RTSP stream, or a placeholder if stream is + # not supported (e.g. WebRTC) + stream = await self.async_create_stream() + if stream: + return await stream.async_get_image(width, height) + return await self.hass.async_add_executor_job(self.placeholder_image) + + @classmethod + @functools.cache + def placeholder_image(cls) -> bytes: + """Return placeholder image to use when no stream is available.""" + return PLACEHOLDER.read_bytes() async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: """Return the source of the stream.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 77b3b24331be0..ff8cffcf7fa02 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -16,6 +16,7 @@ from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, @@ -311,18 +312,22 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" + hvac_mode = self.hvac_mode + if kwargs.get(ATTR_HVAC_MODE) is not None: + hvac_mode = kwargs[ATTR_HVAC_MODE] + await self.async_set_hvac_mode(hvac_mode) low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits: return trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME] - if self.preset_mode == PRESET_ECO or self.hvac_mode == HVAC_MODE_HEAT_COOL: + if self.preset_mode == PRESET_ECO or hvac_mode == HVAC_MODE_HEAT_COOL: if low_temp and high_temp: await trait.set_range(low_temp, high_temp) - elif self.hvac_mode == HVAC_MODE_COOL and temp: + elif hvac_mode == HVAC_MODE_COOL and temp: await trait.set_cool(temp) - elif self.hvac_mode == HVAC_MODE_HEAT and temp: + elif hvac_mode == HVAC_MODE_HEAT and temp: await trait.set_heat(temp) async def async_set_preset_mode(self, preset_mode: str) -> None: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index bccac112d55c3..b257c7b51bb94 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -319,8 +319,7 @@ async def async_step_pubsub( if user_input is not None and not errors: # Create the subscriber id and/or verify it already exists. Note that # the existing id is used, and create call below is idempotent - subscriber_id = data.get(CONF_SUBSCRIBER_ID, "") - if not subscriber_id: + if not (subscriber_id := data.get(CONF_SUBSCRIBER_ID, "")): subscriber_id = _generate_subscription_id(cloud_project_id) _LOGGER.debug("Creating subscriber id '%s'", subscriber_id) # Create a placeholder ConfigEntry to use since with the auth we've already created. @@ -489,7 +488,7 @@ async def async_step_import(self, info: dict[str, Any]) -> FlowResult: config_path = info["nest_conf_path"] if not await self.hass.async_add_executor_job(os.path.isfile, config_path): - self.flow_impl = DOMAIN # type: ignore + self.flow_impl = DOMAIN # type: ignore[assignment] return await self.async_step_link() flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] diff --git a/homeassistant/components/nest/events.py b/homeassistant/components/nest/events.py index 10983768e17d0..752ab0e5069dd 100644 --- a/homeassistant/components/nest/events.py +++ b/homeassistant/components/nest/events.py @@ -25,7 +25,8 @@ # "device_id": "my-device-id", # "type": "camera_motion", # "timestamp": "2021-10-24T19:42:43.304000+00:00", -# "nest_event_id": "KcO1HIR9sPKQ2bqby_vTcCcEov..." +# "nest_event_id": "KcO1HIR9sPKQ2bqby_vTcCcEov...", +# "zones": ["Zone 1"], # }, # ... # } diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index ce1fdd0e7acd4..e0202c63567eb 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -1,4 +1,5 @@ """Support for Nest devices.""" +# mypy: ignore-errors from datetime import datetime, timedelta import logging diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index 79e1bf65c86e0..8b17e7d020ff7 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Nest Thermostat binary sensors.""" +# mypy: ignore-errors + from itertools import chain import logging diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 0b6b7a649a6c4..3bcf1cdee2cc0 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -1,4 +1,6 @@ """Support for Nest Cameras.""" +# mypy: ignore-errors + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 3e0eb5ac16b25..97ed1ee00b91d 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -1,4 +1,6 @@ """Legacy Works with Nest climate implementation.""" +# mypy: ignore-errors + import logging from nest.nest import APIError diff --git a/homeassistant/components/nest/legacy/local_auth.py b/homeassistant/components/nest/legacy/local_auth.py index 6c7f10430934c..a091469cd81e2 100644 --- a/homeassistant/components/nest/legacy/local_auth.py +++ b/homeassistant/components/nest/legacy/local_auth.py @@ -1,4 +1,6 @@ """Local Nest authentication for the legacy api.""" +# mypy: ignore-errors + import asyncio from functools import partial from http import HTTPStatus diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index c5229177b60ca..bb09a40b15e83 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -1,4 +1,6 @@ """Support for Nest Thermostat sensors for the legacy API.""" +# mypy: ignore-errors + import logging from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 832c186db35a2..6968b40156183 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,8 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "media_source"], + "dependencies": ["ffmpeg", "http"], + "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], "codeowners": ["@allenporter"], @@ -13,5 +14,6 @@ { "macaddress": "D8EB46*" }, { "macaddress": "1C53F9*" } ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["google_nest_sdm", "nest"] } diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 2676929f2ded1..af8a4af8ba518 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -134,8 +134,7 @@ async def async_load(self) -> dict | None: """Load data.""" if self._data is None: self._devices = await self._get_devices() - data = await self._store.async_load() - if data is None: + if (data := await self._store.async_load()) is None: _LOGGER.debug("Loaded empty event store") self._data = {} elif isinstance(data, dict): diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index cbba19dac1d32..a0fe869cd3657 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -18,6 +18,11 @@ "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/el.json b/homeassistant/components/nest/translations/el.json index 4fc37801fa075..1628a72173341 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -1,11 +1,61 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "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": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "error": { + "bad_project_id": "\u0395\u03b9\u03c3\u03b1\u03b3\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 (\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf Cloud Console)", + "internal_error": "\u0395\u03c3\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1", + "invalid_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "subscriber_error": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03b7\u03c4\u03ae, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", + "timeout": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "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" + }, "init": { + "data": { + "flow_impl": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2" + }, + "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;", "title": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "link": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "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\u03b7 Nest, [\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 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Nest" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "pubsub": { + "data": { + "cloud_project_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Google Cloud" + }, + "description": "\u0395\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf [Cloud Console]({url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03ad\u03c1\u03b3\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Google Cloud.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Google Cloud" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Nest \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \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" } } }, diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 682ab1728eaa8..7c92631351ef5 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -34,7 +34,7 @@ "flow_impl": "Provider" }, "description": "Scegli il metodo di autenticazione", - "title": "Fornitore di autenticazione" + "title": "Provider di autenticazione" }, "link": { "data": { diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 6d312fa98c1c9..54b558493b854 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -1,33 +1,70 @@ { "config": { "abort": { - "authorize_url_timeout": "Excedido tempo limite de url de autoriza\u00e7\u00e3o" + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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": "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": { + "default": "Autenticado com sucesso" }, "error": { + "bad_project_id": "Insira um ID de projeto do Cloud v\u00e1lido (verifique o Console do Cloud)", "internal_error": "Erro interno ao validar o c\u00f3digo", + "invalid_pin": "C\u00f3digo PIN", + "subscriber_error": "Erro de assinante desconhecido, veja os logs", "timeout": "Excedido tempo limite para validar c\u00f3digo", - "unknown": "Erro desconhecido ao validar o c\u00f3digo" + "unknown": "Erro inesperado", + "wrong_project_id": "Insira um ID de projeto do Cloud v\u00e1lido (ID do projeto de acesso ao dispositivo encontrado)" }, "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" + }, "init": { "data": { "flow_impl": "Provedor" }, - "description": "Escolha atrav\u00e9s de qual provedor de autentica\u00e7\u00e3o voc\u00ea deseja autenticar com o Nest.", + "description": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o", "title": "Provedor de Autentica\u00e7\u00e3o" }, "link": { "data": { "code": "C\u00f3digo PIN" }, - "description": "Para vincular sua conta do Nest, [autorize sua conta] ( {url} ). \n\n Ap\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo PIN fornecido abaixo.", + "description": "Para vincular sua conta do Nest, [autorize sua conta]({url}). \n\n Ap\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo PIN fornecido abaixo.", "title": "Link da conta Nest" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "pubsub": { + "data": { + "cloud_project_id": "ID do projeto do Google Cloud" + }, + "description": "Visite o [Cloud Console]( {url} ) para encontrar o ID do projeto do Google Cloud.", + "title": "Configurar o Google Cloud" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o Nest precisa re-autenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" } } }, "device_automation": { "trigger_type": { - "camera_motion": "Movimento detectado" + "camera_motion": "Movimento detectado", + "camera_person": "Pessoa detectada", + "camera_sound": "Som detectado", + "doorbell_chime": "Campainha pressionada" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/sk.json b/homeassistant/components/nest/translations/sk.json new file mode 100644 index 0000000000000..029432864bf1b --- /dev/null +++ b/homeassistant/components/nest/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "auth": { + "data": { + "code": "Pr\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json index f2ee64e7fc4b6..9ab8349670eab 100644 --- a/homeassistant/components/nest/translations/uk.json +++ b/homeassistant/components/nest/translations/uk.json @@ -5,7 +5,7 @@ "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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": { @@ -44,7 +44,7 @@ "device_automation": { "trigger_type": { "camera_motion": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445", - "camera_person": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043b\u044e\u0434\u0438\u043d\u0438", + "camera_person": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043b\u044e\u0434\u0438\u043d\u0443", "camera_sound": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0437\u0432\u0443\u043a", "doorbell_chime": "\u041d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0434\u0437\u0432\u0456\u043d\u043a\u0430" } diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index afae41f7d7a6c..c52a22e697011 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -6,7 +6,7 @@ "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": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "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": { diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index ac11ee554a898..f6e43b296537a 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -31,10 +31,7 @@ config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType @@ -54,7 +51,6 @@ OAUTH2_AUTHORIZE, OAUTH2_TOKEN, PLATFORMS, - WEBHOOK_ACTIVATION, WEBHOOK_DEACTIVATION, WEBHOOK_PUSH_TYPE, ) @@ -150,8 +146,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - _webhook_retries = 0 - async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, ) -> None: @@ -171,11 +165,6 @@ async def unregister_webhook( "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID] ) - nonlocal _webhook_retries - if _webhook_retries < MAX_WEBHOOK_RETRIES: - _webhook_retries += 1 - async_call_later(hass, 30, register_webhook) - async def register_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, ) -> None: @@ -184,14 +173,7 @@ async def register_webhook( hass.config_entries.async_update_entry(entry, data=data) if cloud.async_active_subscription(hass): - if CONF_CLOUDHOOK_URL not in entry.data: - webhook_url = await cloud.async_create_cloudhook( - hass, entry.data[CONF_WEBHOOK_ID] - ) - data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} - hass.config_entries.async_update_entry(entry, data=data) - else: - webhook_url = entry.data[CONF_CLOUDHOOK_URL] + webhook_url = await async_cloudhook_generate_url(hass, entry) else: webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) @@ -204,32 +186,15 @@ async def register_webhook( ) return - try: - webhook_register( - hass, - DOMAIN, - "Netatmo", - entry.data[CONF_WEBHOOK_ID], - async_handle_webhook, - ) - - async def handle_event(event: dict) -> None: - """Handle webhook events.""" - if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: - if activation_listener is not None: - activation_listener() - - if activation_timeout is not None: - activation_timeout() - - activation_listener = async_dispatcher_connect( - hass, - f"signal-{DOMAIN}-webhook-None", - handle_event, - ) - - activation_timeout = async_call_later(hass, 30, unregister_webhook) + webhook_register( + hass, + DOMAIN, + "Netatmo", + entry.data[CONF_WEBHOOK_ID], + async_handle_webhook, + ) + try: await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url) _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: @@ -239,10 +204,24 @@ async def handle_event(event: dict) -> None: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) ) - if hass.state == CoreState.running: - await register_webhook(None) + async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: + if state is cloud.CloudConnectionState.CLOUD_CONNECTED: + await register_webhook(None) + + if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED: + await unregister_webhook(None) + async_call_later(hass, 30, register_webhook) + + if cloud.async_active_subscription(hass): + if cloud.async_is_connected(hass): + await register_webhook(None) + cloud.async_listen_connection_change(hass, manage_cloudhook) + else: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, register_webhook) + if hass.state == CoreState.running: + await register_webhook(None) + else: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, register_webhook) hass.services.async_register(DOMAIN, "register_webhook", register_webhook) hass.services.async_register(DOMAIN, "unregister_webhook", unregister_webhook) @@ -252,6 +231,18 @@ async def handle_event(event: dict) -> None: return True +async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: + """Generate the full URL for a webhook_id.""" + if CONF_CLOUDHOOK_URL not in entry.data: + webhook_url = await cloud.async_create_cloudhook( + hass, entry.data[CONF_WEBHOOK_ID] + ) + data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url} + hass.config_entries.async_update_entry(entry, data=data) + return webhook_url + return str(entry.data[CONF_CLOUDHOOK_URL]) + + async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle signals of config entry being updated.""" async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 380571aba70a2..7fa9fe02956e0 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -55,9 +55,6 @@ async def async_setup_entry( """Set up the Netatmo camera platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - await data_handler.register_data_class( - CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None - ) data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) if not data_class or not data_class.raw_data: diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index c8b5e01e5dbb2..623b7d0573a5c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -124,9 +124,6 @@ async def async_setup_entry( """Set up the Netatmo energy platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - await data_handler.register_data_class( - CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None - ) climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) if not climate_topology or climate_topology.raw_data == {}: diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index ace5934adbd3e..1d6345506c1e9 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -74,7 +74,7 @@ class NetatmoDataClass: name: str interval: int next_scan: float - subscriptions: list[CALLBACK_TYPE] + subscriptions: list[CALLBACK_TYPE | None] class NetatmoDataHandler: @@ -105,6 +105,18 @@ async def async_setup(self) -> None: ) ) + await asyncio.gather( + *[ + self.register_data_class(data_class, data_class, None) + for data_class in ( + CLIMATE_TOPOLOGY_CLASS_NAME, + CAMERA_DATA_CLASS_NAME, + WEATHERSTATION_DATA_CLASS_NAME, + HOMECOACH_DATA_CLASS_NAME, + ) + ] + ) + async def async_update(self, event_time: datetime) -> None: """ Update device. @@ -172,7 +184,7 @@ async def register_data_class( self, data_class_name: str, data_class_entry: str, - update_callback: CALLBACK_TYPE, + update_callback: CALLBACK_TYPE | None, **kwargs: Any, ) -> None: """Register data class.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 9d83aa0297709..58b1a0d4f43bc 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -34,10 +34,6 @@ async def async_setup_entry( ) -> None: """Set up the Netatmo camera light platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - - await data_handler.register_data_class( - CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None - ) data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) if not data_class or data_class.raw_data == {}: diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 2f133f1cdfa68..e801d941a74ad 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -24,5 +24,6 @@ "Welcome" ] }, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyatmo"] } \ No newline at end of file diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 6c5f3eef00ad2..56f33b04432b1 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -37,9 +37,6 @@ async def async_setup_entry( """Set up the Netatmo energy platform schedule selector.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - await data_handler.register_data_class( - CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None - ) climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) if not climate_topology or climate_topology.raw_data == {}: diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index defcd757d0a7c..41ae27b2992d5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -386,7 +386,6 @@ async def find_entities(data_class_name: str) -> list: WEATHERSTATION_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, ): - await data_handler.register_data_class(data_class_name, data_class_name, None) data_class = data_handler.data.get(data_class_name) if data_class and data_class.raw_data: @@ -840,15 +839,16 @@ def async_update_callback(self) -> None: elif self.entity_description.key == "guststrength": data = self._data.get_latest_gust_strengths() - if data is None: - if self.state is None: - return - _LOGGER.debug( - "No station provides %s data in the area %s", - self.entity_description.key, - self._area_name, - ) - self._attr_native_value = None + if not data: + if self.available: + _LOGGER.error( + "No station provides %s data in the area %s", + self.entity_description.key, + self._area_name, + ) + self._attr_native_value = None + + self._attr_available = False return if values := [x for x in data.values() if x is not None]: diff --git a/homeassistant/components/netatmo/translations/cs.json b/homeassistant/components/netatmo/translations/cs.json index 7857e345165b0..5233fc3e2397b 100644 --- a/homeassistant/components/netatmo/translations/cs.json +++ b/homeassistant/components/netatmo/translations/cs.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "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." }, "create_entry": { @@ -12,9 +13,23 @@ "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "description": "Integrace Netatmo pot\u0159ebuje znovu ov\u011b\u0159it v\u00e1\u0161 \u00fa\u010det", + "title": "Znovu ov\u011b\u0159it integraci" } } }, + "device_automation": { + "trigger_subtype": { + "away": "pry\u010d", + "hg": "ochrana proti mrazu", + "schedule": "pl\u00e1n" + }, + "trigger_type": { + "set_point": "C\u00edlov\u00e1 teplota {entity_name} nastavena ru\u010dn\u011b" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/el.json b/homeassistant/components/netatmo/translations/el.json index 364e390ddc26e..1fcf961a3631b 100644 --- a/homeassistant/components/netatmo/translations/el.json +++ b/homeassistant/components/netatmo/translations/el.json @@ -1,4 +1,25 @@ { + "config": { + "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Netatmo \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \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" + } + } + }, "device_automation": { "trigger_subtype": { "away": "\u03b5\u03ba\u03c4\u03cc\u03c2", @@ -14,10 +35,36 @@ "outdoor": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03bf\u03cd \u03c7\u03ce\u03c1\u03bf\u03c5", "person": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf", "person_away": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c6\u03cd\u03b3\u03b5\u03b9", + "set_point": "\u0397 \u03c3\u03c4\u03bf\u03c7\u03b5\u03c5\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name} \u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1", "therm_mode": "{entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03c3\u03b5 \"{subtype}\"", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "vehicle": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03c7\u03b7\u03bc\u03b1" } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2", + "lat_ne": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u0392\u03bf\u03c1\u03b5\u03b9\u03bf\u03b1\u03bd\u03b1\u03c4\u03bf\u03bb\u03b9\u03ba\u03ae \u03b3\u03c9\u03bd\u03af\u03b1", + "lat_sw": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u039d\u03bf\u03c4\u03b9\u03bf\u03b4\u03c5\u03c4\u03b9\u03ba\u03ae \u03b3\u03c9\u03bd\u03af\u03b1", + "lon_ne": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2 \u0392\u03bf\u03c1\u03b5\u03b9\u03bf\u03b1\u03bd\u03b1\u03c4\u03bf\u03bb\u03b9\u03ba\u03ae \u03b3\u03c9\u03bd\u03af\u03b1", + "lon_sw": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2 \u039d\u03bf\u03c4\u03b9\u03bf\u03b4\u03c5\u03c4\u03b9\u03ba\u03ae \u03b3\u03c9\u03bd\u03af\u03b1", + "mode": "\u03a5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03cc\u03c2", + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b7\u03bc\u03cc\u03c3\u03b9\u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae.", + "title": "\u0394\u03b7\u03bc\u03cc\u03c3\u03b9\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2", + "weather_areas": "\u039a\u03b1\u03b9\u03c1\u03b9\u03ba\u03ad\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ad\u03c2" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03b7\u03bc\u03cc\u03c3\u03b9\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd.", + "title": "\u0394\u03b7\u03bc\u03cc\u03c3\u03b9\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd Netatmo" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index c2a702277fede..ae781d86ca8a9 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -52,7 +52,7 @@ "lon_ne": "\u00c9szakkeleti sarok hossz\u00fas\u00e1gi fok", "lon_sw": "D\u00e9lnyugati sarok hossz\u00fas\u00e1ggi fok", "mode": "Sz\u00e1m\u00edt\u00e1s", - "show_on_map": "Mutasd a t\u00e9rk\u00e9pen" + "show_on_map": "Megjelen\u00edt\u00e9s a t\u00e9rk\u00e9pen" }, "description": "\u00c1ll\u00edtson be egy nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151t egy ter\u00fclethez.", "title": "Netatmo nyilv\u00e1nos id\u0151j\u00e1r\u00e1s-\u00e9rz\u00e9kel\u0151" diff --git a/homeassistant/components/netatmo/translations/pt-BR.json b/homeassistant/components/netatmo/translations/pt-BR.json index 77e55a889c4c6..32cc610f59693 100644 --- a/homeassistant/components/netatmo/translations/pt-BR.json +++ b/homeassistant/components/netatmo/translations/pt-BR.json @@ -1,8 +1,69 @@ { "config": { + "abort": { + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, "reauth_confirm": { - "title": "Reautenticar integra\u00e7\u00e3o" + "description": "A integra\u00e7\u00e3o Netatmo precisa autenticar novamente sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + }, + "device_automation": { + "trigger_subtype": { + "away": "fora", + "hg": "protetor de geada", + "schedule": "hor\u00e1rio" + }, + "trigger_type": { + "alarm_started": "{entity_name} detectou um alarme", + "animal": "{entity_name} detectou um animal", + "cancel_set_point": "{entity_name} retomou sua programa\u00e7\u00e3o", + "human": "{entity_name} detectou um humano", + "movement": "{entity_name} detectou movimento", + "outdoor": "{entity_name} detectou um evento ao ar livre", + "person": "{entity_name} detectou uma pessoa", + "person_away": "{entity_name} detectou que uma pessoa saiu", + "set_point": "Temperatura alvo {entity_name} definida manualmente", + "therm_mode": "{entity_name} mudou para \" {subtype} \"", + "turned_off": "{entity_name} for desligado", + "turned_on": "{entity_name} for ligado", + "vehicle": "{entity_name} detectou um ve\u00edculo" + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nome da \u00e1rea", + "lat_ne": "Latitude nordeste", + "lat_sw": "Latitude sudoeste", + "lon_ne": "Longitude nordeste", + "lon_sw": "Longitude sudoeste", + "mode": "C\u00e1lculo", + "show_on_map": "Mostrar no mapa?" + }, + "description": "Configure um sensor meteorol\u00f3gico p\u00fablico para uma \u00e1rea.", + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "Nome da \u00e1rea", + "weather_areas": "\u00c1reas meteorol\u00f3gicas" + }, + "description": "Configurar sensores meteorol\u00f3gicos p\u00fablicos.", + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" } } } diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index 1bb004b546493..ff089be667f77 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -52,7 +52,7 @@ "lon_ne": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430 (\u0441\u0435\u0432\u0435\u0440\u043e-\u0432\u043e\u0441\u0442\u043e\u0447\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", "lon_sw": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", "mode": "\u0420\u0430\u0441\u0447\u0435\u0442", - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0438.", "title": "\u041e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b Netatmo" diff --git a/homeassistant/components/netatmo/translations/sk.json b/homeassistant/components/netatmo/translations/sk.json new file mode 100644 index 0000000000000..0f73e9340d9d4 --- /dev/null +++ b/homeassistant/components/netatmo/translations/sk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "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" + } + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/uk.json b/homeassistant/components/netatmo/translations/uk.json index b8c439edfde4f..9d6065a4841cb 100644 --- a/homeassistant/components/netatmo/translations/uk.json +++ b/homeassistant/components/netatmo/translations/uk.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "create_entry": { "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index f8d181be5d3c7..84bb2dcffa3a2 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -5,7 +5,7 @@ "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": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 34fbf45c52999..5be37a358eda9 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": ["netdata==1.0.1"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["netdata"] } diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 26c7f4f1a1a39..2842157f578ad 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,4 +1,5 @@ """Support for Netgear routers.""" +from datetime import timedelta import logging from homeassistant.config_entries import ConfigEntry @@ -6,19 +7,29 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr - -from .const import DOMAIN, PLATFORMS +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + DOMAIN, + KEY_COORDINATOR, + KEY_COORDINATOR_TRAFFIC, + KEY_ROUTER, + PLATFORMS, +) from .errors import CannotLoginException from .router import NetgearRouter _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=30) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Netgear component.""" router = NetgearRouter(hass, entry) try: - await router.async_setup() + if not await router.async_setup(): + raise ConfigEntryNotReady except CannotLoginException as ex: raise ConfigEntryNotReady from ex @@ -37,7 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = router entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -49,9 +59,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=router.device_name, model=router.model, sw_version=router.firmware_version, + hw_version=router.hardware_version, configuration_url=f"http://{entry.data[CONF_HOST]}/", ) + async def async_update_devices() -> bool: + """Fetch data from the router.""" + return await router.async_update_device_trackers() + + async def async_update_traffic_meter() -> dict: + """Fetch data from the router.""" + return await router.async_get_traffic_meter() + + # Create update coordinators + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Devices", + update_method=async_update_devices, + update_interval=SCAN_INTERVAL, + ) + coordinator_traffic_meter = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Traffic meter", + update_method=async_update_traffic_meter, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_config_entry_first_refresh() + await coordinator_traffic_meter.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + KEY_ROUTER: router, + KEY_COORDINATOR: coordinator, + KEY_COORDINATOR_TRAFFIC: coordinator_traffic_meter, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -62,7 +106,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) diff --git a/homeassistant/components/netgear/button.py b/homeassistant/components/netgear/button.py new file mode 100644 index 0000000000000..e14600ff52b40 --- /dev/null +++ b/homeassistant/components/netgear/button.py @@ -0,0 +1,82 @@ +"""Support for Netgear Button.""" +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER +from .router import NetgearRouter, NetgearRouterEntity + + +@dataclass +class NetgearButtonEntityDescriptionRequired: + """Required attributes of NetgearButtonEntityDescription.""" + + action: Callable[[NetgearRouter], Callable[[], Coroutine[Any, Any, None]]] + + +@dataclass +class NetgearButtonEntityDescription( + ButtonEntityDescription, NetgearButtonEntityDescriptionRequired +): + """Class describing Netgear button entities.""" + + +BUTTONS = [ + NetgearButtonEntityDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + action=lambda router: router.async_reboot, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up button for Netgear component.""" + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] + async_add_entities( + NetgearRouterButtonEntity(coordinator, router, entity_description) + for entity_description in BUTTONS + ) + + +class NetgearRouterButtonEntity(NetgearRouterEntity, ButtonEntity): + """Netgear Router button entity.""" + + entity_description: NetgearButtonEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: NetgearRouter, + entity_description: NetgearButtonEntityDescription, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator, router) + self.entity_description = entity_description + self._name = f"{router.device_name} {entity_description.name}" + self._unique_id = f"{router.serial_number}-{entity_description.key}" + + async def async_press(self) -> None: + """Triggers the button press service.""" + async_action = self.entity_description.action(self._router) + await async_action() + + @callback + def async_update_device(self) -> None: + """Update the Netgear device.""" diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index 5f8944f96d673..f2e0263a4e469 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -5,10 +5,14 @@ DOMAIN = "netgear" -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] CONF_CONSIDER_HOME = "consider_home" +KEY_ROUTER = "router" +KEY_COORDINATOR = "coordinator" +KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" + DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index e3beb005845d7..72699768f84f9 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -8,9 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DEVICE_ICONS -from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry +from .const import DEVICE_ICONS, DOMAIN, KEY_COORDINATOR, KEY_ROUTER +from .router import NetgearBaseEntity, NetgearRouter _LOGGER = logging.getLogger(__name__) @@ -19,19 +20,42 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for Netgear component.""" + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] + tracked = set() - def generate_classes(router: NetgearRouter, device: dict): - return [NetgearScannerEntity(router, device)] + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + if not coordinator.data: + return + + new_entities = [] + + for mac, device in router.devices.items(): + if mac in tracked: + continue + + new_entities.append(NetgearScannerEntity(coordinator, router, device)) + tracked.add(mac) - async_setup_netgear_entry(hass, entry, async_add_entities, generate_classes) + if new_entities: + async_add_entities(new_entities) + entry.async_on_unload(coordinator.async_add_listener(new_device_callback)) -class NetgearScannerEntity(NetgearDeviceEntity, ScannerEntity): + coordinator.data = True + new_device_callback() + + +class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): """Representation of a device connected to a Netgear router.""" - def __init__(self, router: NetgearRouter, device: dict) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict + ) -> None: """Initialize a Netgear device.""" - super().__init__(router, device) + super().__init__(coordinator, router, device) self._hostname = self.get_hostname() self._icon = DEVICE_ICONS.get(device["device_type"], "mdi:help-network") @@ -49,8 +73,6 @@ def async_update_device(self) -> None: self._active = self._device["active"] self._icon = DEVICE_ICONS.get(self._device["device_type"], "mdi:help-network") - self.async_write_ha_state() - @property def is_connected(self): """Return true if the device is connected to the router.""" diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 2a81bc0b6a995..b2c7ddf6be2b0 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -11,5 +11,6 @@ "manufacturer": "NETGEAR, Inc.", "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" } - ] + ], + "loggers": ["pynetgear"] } diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 5226218a62382..9e44495aa624e 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Callable +import asyncio from datetime import timedelta import logging @@ -19,13 +19,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util from .const import ( @@ -37,8 +35,6 @@ ) from .errors import CannotLoginException -SCAN_INTERVAL = timedelta(seconds=30) - _LOGGER = logging.getLogger(__name__) @@ -58,47 +54,6 @@ def get_api( return api -@callback -def async_setup_netgear_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - entity_class_generator: Callable[[NetgearRouter, dict], list], -) -> None: - """Set up device tracker for Netgear component.""" - router = hass.data[DOMAIN][entry.unique_id] - tracked = set() - - @callback - def _async_router_updated(): - """Update the values of the router.""" - async_add_new_entities( - router, async_add_entities, tracked, entity_class_generator - ) - - entry.async_on_unload( - async_dispatcher_connect(hass, router.signal_device_new, _async_router_updated) - ) - - _async_router_updated() - - -@callback -def async_add_new_entities(router, async_add_entities, tracked, entity_class_generator): - """Add new tracker entities from the router.""" - new_tracked = [] - - for mac, device in router.devices.items(): - if mac in tracked: - continue - - new_tracked.extend(entity_class_generator(router, device)) - tracked.add(mac) - - if new_tracked: - async_add_entities(new_tracked, True) - - class NetgearRouter: """Representation of a Netgear router.""" @@ -115,9 +70,11 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self._password = entry.data[CONF_PASSWORD] self._info = None - self.model = None - self.device_name = None - self.firmware_version = None + self.model = "" + self.device_name = "" + self.firmware_version = "" + self.hardware_version = "" + self.serial_number = "" self.method_version = 1 consider_home_int = entry.options.get( @@ -126,7 +83,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self._consider_home = timedelta(seconds=consider_home_int) self._api: Netgear = None - self._attrs = {} + self._api_lock = asyncio.Lock() self.devices = {} @@ -141,9 +98,14 @@ def _setup(self) -> None: ) self._info = self._api.get_info() + if self._info is None: + return False + self.device_name = self._info.get("DeviceName", DEFAULT_NAME) self.model = self._info.get("ModelName") self.firmware_version = self._info.get("Firmwareversion") + self.hardware_version = self._info.get("Hardwareversion") + self.serial_number = self._info["SerialNumber"] for model in MODELS_V2: if self.model.startswith(model): @@ -157,9 +119,13 @@ def _setup(self) -> None: ) self.method_version = 1 - async def async_setup(self) -> None: + return True + + async def async_setup(self) -> bool: """Set up a Netgear router.""" - await self.hass.async_add_executor_job(self._setup) + async with self._api_lock: + if not await self.hass.async_add_executor_job(self._setup): + return False # set already known devices to away instead of unavailable device_registry = dr.async_get(self.hass) @@ -182,26 +148,24 @@ async def async_setup(self) -> None: "ip": None, "ssid": None, "conn_ap_mac": None, + "allow_or_block": None, } - await self.async_update_device_trackers() - self.entry.async_on_unload( - async_track_time_interval( - self.hass, self.async_update_device_trackers, SCAN_INTERVAL - ) - ) - - async_dispatcher_send(self.hass, self.signal_device_new) + return True async def async_get_attached_devices(self) -> list: """Get the devices connected to the router.""" if self.method_version == 1: + async with self._api_lock: + return await self.hass.async_add_executor_job( + self._api.get_attached_devices + ) + + async with self._api_lock: return await self.hass.async_add_executor_job( - self._api.get_attached_devices + self._api.get_attached_devices_2 ) - return await self.hass.async_add_executor_job(self._api.get_attached_devices_2) - async def async_update_device_trackers(self, now=None) -> None: """Update Netgear devices.""" new_device = False @@ -228,21 +192,27 @@ async def async_update_device_trackers(self, now=None) -> None: for device in self.devices.values(): device["active"] = now - device["last_seen"] <= self._consider_home - async_dispatcher_send(self.hass, self.signal_device_update) - if new_device: _LOGGER.debug("Netgear tracker: new device found") - async_dispatcher_send(self.hass, self.signal_device_new) - @property - def signal_device_new(self) -> str: - """Event specific per Netgear entry to signal new device.""" - return f"{DOMAIN}-{self._host}-device-new" + return new_device - @property - def signal_device_update(self) -> str: - """Event specific per Netgear entry to signal updates in devices.""" - return f"{DOMAIN}-{self._host}-device-update" + async def async_get_traffic_meter(self) -> None: + """Get the traffic meter data of the router.""" + async with self._api_lock: + return await self.hass.async_add_executor_job(self._api.get_traffic_meter) + + async def async_allow_block_device(self, mac: str, allow_block: str) -> None: + """Allow or block a device connected to the router.""" + async with self._api_lock: + await self.hass.async_add_executor_job( + self._api.allow_block_device, mac, allow_block + ) + + async def async_reboot(self) -> None: + """Reboot the router.""" + async with self._api_lock: + await self.hass.async_add_executor_job(self._api.reboot) @property def port(self) -> int: @@ -255,17 +225,19 @@ def ssl(self) -> bool: return self._api.ssl -class NetgearDeviceEntity(Entity): +class NetgearBaseEntity(CoordinatorEntity): """Base class for a device connected to a Netgear router.""" - def __init__(self, router: NetgearRouter, device: dict) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict + ) -> None: """Initialize a Netgear device.""" + super().__init__(coordinator) self._router = router self._device = device self._mac = device["mac"] self._name = self.get_device_name() self._device_name = self._name - self._unique_id = self._mac self._active = device["active"] def get_device_name(self): @@ -281,16 +253,33 @@ def get_device_name(self): def async_update_device(self) -> None: """Update the Netgear device.""" - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_update_device() + super()._handle_coordinator_update() @property def name(self) -> str: """Return the name.""" return self._name + +class NetgearDeviceEntity(NetgearBaseEntity): + """Base class for a device connected to a Netgear router.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, router: NetgearRouter, device: dict + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator, router, device) + self._unique_id = self._mac + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + @property def device_info(self) -> DeviceInfo: """Return the device information.""" @@ -301,17 +290,43 @@ def device_info(self) -> DeviceInfo: via_device=(DOMAIN, self._router.unique_id), ) + +class NetgearRouterEntity(CoordinatorEntity): + """Base class for a Netgear router entity.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, router: NetgearRouter + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator) + self._router = router + self._name = router.device_name + self._unique_id = router.serial_number + + @abstractmethod + @callback + def async_update_device(self) -> None: + """Update the Netgear device.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_update_device() + super()._handle_coordinator_update() + @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_added_to_hass(self): - """Register state update callback.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._router.signal_device_update, - self.async_update_device, - ) + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self._router.unique_id)}, ) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 227e6e28b092a..16c63a8cdcbb5 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -1,28 +1,35 @@ """Support for Netgear routers.""" +from collections.abc import Callable +from dataclasses import dataclass + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import DATA_MEGABYTES, PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry +from .const import DOMAIN, KEY_COORDINATOR, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER +from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity SENSOR_TYPES = { "type": SensorEntityDescription( key="type", name="link type", entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:lan", ), "link_rate": SensorEntityDescription( key="link_rate", name="link rate", native_unit_of_measurement="Mbps", entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:speedometer", ), "signal": SensorEntityDescription( key="signal", @@ -35,28 +42,216 @@ key="ssid", name="ssid", entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi-marker", ), "conn_ap_mac": SensorEntityDescription( key="conn_ap_mac", name="access point mac", entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:router-network", ), } +@dataclass +class NetgearSensorEntityDescription(SensorEntityDescription): + """Class describing Netgear sensor entities.""" + + value: Callable = lambda data: data + index: int = 0 + + +SENSOR_TRAFFIC_TYPES = [ + NetgearSensorEntityDescription( + key="NewTodayUpload", + name="Upload today", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + ), + NetgearSensorEntityDescription( + key="NewTodayDownload", + name="Download today", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + ), + NetgearSensorEntityDescription( + key="NewYesterdayUpload", + name="Upload yesterday", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + ), + NetgearSensorEntityDescription( + key="NewYesterdayDownload", + name="Download yesterday", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + ), + NetgearSensorEntityDescription( + key="NewWeekUpload", + name="Upload week", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewWeekUpload", + name="Upload week average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=1, + value=lambda data: data[1] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewWeekDownload", + name="Download week", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewWeekDownload", + name="Download week average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=1, + value=lambda data: data[1] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewMonthUpload", + name="Upload month", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewMonthUpload", + name="Upload month average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=1, + value=lambda data: data[1] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewMonthDownload", + name="Download month", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewMonthDownload", + name="Download month average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=1, + value=lambda data: data[1] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewLastMonthUpload", + name="Upload last month", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewLastMonthUpload", + name="Upload last month average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload", + index=1, + value=lambda data: data[1] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewLastMonthDownload", + name="Download last month", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=0, + value=lambda data: data[0] if data is not None else None, + ), + NetgearSensorEntityDescription( + key="NewLastMonthDownload", + name="Download last month average", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download", + index=1, + value=lambda data: data[1] if data is not None else None, + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for Netgear component.""" + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] + coordinator_traffic = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_TRAFFIC] + + # Router entities + router_entities = [] + + for description in SENSOR_TRAFFIC_TYPES: + router_entities.append( + NetgearRouterSensorEntity(coordinator_traffic, router, description) + ) + + async_add_entities(router_entities) + + # Entities per network device + tracked = set() + sensors = ["type", "link_rate", "signal"] + if router.method_version == 2: + sensors.extend(["ssid", "conn_ap_mac"]) + + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + if not coordinator.data: + return + + new_entities = [] + + for mac, device in router.devices.items(): + if mac in tracked: + continue + + new_entities.extend( + [ + NetgearSensorEntity(coordinator, router, device, attribute) + for attribute in sensors + ] + ) + tracked.add(mac) - def generate_sensor_classes(router: NetgearRouter, device: dict): - sensors = ["type", "link_rate", "signal"] - if router.method_version == 2: - sensors.extend(["ssid", "conn_ap_mac"]) + if new_entities: + async_add_entities(new_entities) - return [NetgearSensorEntity(router, device, attribute) for attribute in sensors] + entry.async_on_unload(coordinator.async_add_listener(new_device_callback)) - async_setup_netgear_entry(hass, entry, async_add_entities, generate_sensor_classes) + coordinator.data = True + new_device_callback() class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): @@ -64,9 +259,15 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): _attr_entity_registry_enabled_default = False - def __init__(self, router: NetgearRouter, device: dict, attribute: str) -> None: + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: NetgearRouter, + device: dict, + attribute: str, + ) -> None: """Initialize a Netgear device.""" - super().__init__(router, device) + super().__init__(coordinator, router, device) self._attribute = attribute self.entity_description = SENSOR_TYPES[self._attribute] self._name = f"{self.get_device_name()} {self.entity_description.name}" @@ -86,4 +287,36 @@ def async_update_device(self) -> None: if self._device.get(self._attribute) is not None: self._state = self._device[self._attribute] - self.async_write_ha_state() + +class NetgearRouterSensorEntity(NetgearRouterEntity, SensorEntity): + """Representation of a device connected to a Netgear router.""" + + _attr_entity_registry_enabled_default = False + entity_description: NetgearSensorEntityDescription + + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: NetgearRouter, + entity_description: NetgearSensorEntityDescription, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator, router) + self.entity_description = entity_description + self._name = f"{router.device_name} {entity_description.name}" + self._unique_id = f"{router.serial_number}-{entity_description.key}-{entity_description.index}" + + self._value = None + self.async_update_device() + + @property + def native_value(self): + """Return the state of the sensor.""" + return self._value + + @callback + def async_update_device(self) -> None: + """Update the Netgear device.""" + if self.coordinator.data is not None: + data = self.coordinator.data.get(self.entity_description.key) + self._value = self.entity_description.value(data) diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index ce575277dad8e..7a81d414e2f32 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Default host: {host}\nDefault port: {port}\nDefault username: {username}", + "description": "Default host: {host}\nDefault username: {username}", "data": { "host": "[%key:common::config_flow::data::host%] (Optional)", "username": "[%key:common::config_flow::data::username%] (Optional)", diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py new file mode 100644 index 0000000000000..cf8cd835f8966 --- /dev/null +++ b/homeassistant/components/netgear/switch.py @@ -0,0 +1,106 @@ +"""Support for Netgear switches.""" +import logging + +from pynetgear import ALLOW, BLOCK + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER +from .router import NetgearDeviceEntity, NetgearRouter + +_LOGGER = logging.getLogger(__name__) + + +SWITCH_TYPES = [ + SwitchEntityDescription( + key="allow_or_block", + name="Allowed on network", + icon="mdi:block-helper", + entity_category=EntityCategory.CONFIG, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up switches for Netgear component.""" + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] + tracked = set() + + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + new_entities = [] + if not coordinator.data: + return + + for mac, device in router.devices.items(): + if mac in tracked: + continue + + new_entities.extend( + [ + NetgearAllowBlock(coordinator, router, device, entity_description) + for entity_description in SWITCH_TYPES + ] + ) + tracked.add(mac) + + if new_entities: + async_add_entities(new_entities) + + entry.async_on_unload(coordinator.async_add_listener(new_device_callback)) + + coordinator.data = True + new_device_callback() + + +class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): + """Allow or Block a device from the network.""" + + _attr_entity_registry_enabled_default = False + + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: NetgearRouter, + device: dict, + entity_description: SwitchEntityDescription, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator, router, device) + self.entity_description = entity_description + self._name = f"{self.get_device_name()} {self.entity_description.name}" + self._unique_id = f"{self._mac}-{self.entity_description.key}" + self._state = None + self.async_update_device() + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._router.async_allow_block_device(self._mac, ALLOW) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._router.async_allow_block_device(self._mac, BLOCK) + + @callback + def async_update_device(self) -> None: + """Update the Netgear device.""" + self._device = self._router.devices[self._mac] + self._active = self._device["active"] + if self._device[self.entity_description.key] is None: + self._state = None + else: + self._state = self._device[self.entity_description.key] == "Allow" diff --git a/homeassistant/components/netgear/translations/ca.json b/homeassistant/components/netgear/translations/ca.json index 48de8c99684cc..6d5edbd8e1ffe 100644 --- a/homeassistant/components/netgear/translations/ca.json +++ b/homeassistant/components/netgear/translations/ca.json @@ -15,7 +15,7 @@ "ssl": "Utilitza un certificat SSL", "username": "Nom d'usuari (opcional)" }, - "description": "Amfitri\u00f3 predeterminat: {host}\nPort predeterminat: {port}\nNom d'usuari predeterminat: {username}", + "description": "Amfitri\u00f3 predeterminat: {host}\nNom d'usuari predeterminat: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/de.json b/homeassistant/components/netgear/translations/de.json index d1ee1310cadfe..b742fb36ec5c1 100644 --- a/homeassistant/components/netgear/translations/de.json +++ b/homeassistant/components/netgear/translations/de.json @@ -15,7 +15,7 @@ "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername (Optional)" }, - "description": "Standardhost: {host}\nStandardport: {port}\nStandardbenutzername: {username}", + "description": "Standardhost: {host}\nStandardbenutzername: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/el.json b/homeassistant/components/netgear/translations/el.json new file mode 100644 index 0000000000000..141f8f31ddd96 --- /dev/null +++ b/homeassistant/components/netgear/translations/el.json @@ -0,0 +1,34 @@ +{ + "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": { + "config": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2: \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\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 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "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 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + }, + "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1: {port}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u0395\u03be\u03b5\u03c4\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ce\u03c1\u03b1 \u03c3\u03c4\u03bf \u03c3\u03c0\u03af\u03c4\u03b9 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/en.json b/homeassistant/components/netgear/translations/en.json index f9c2dbf2c9147..42b014d9ed3a2 100644 --- a/homeassistant/components/netgear/translations/en.json +++ b/homeassistant/components/netgear/translations/en.json @@ -15,7 +15,7 @@ "ssl": "Uses an SSL certificate", "username": "Username (Optional)" }, - "description": "Default host: {host}\nDefault port: {port}\nDefault username: {username}", + "description": "Default host: {host}\nDefault username: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/et.json b/homeassistant/components/netgear/translations/et.json index ad100c4b83ebc..dcbaf4bcb2e8b 100644 --- a/homeassistant/components/netgear/translations/et.json +++ b/homeassistant/components/netgear/translations/et.json @@ -15,7 +15,7 @@ "ssl": "Kasutusel on SSL sert", "username": "Kasutajanimi (valikuline)" }, - "description": "Vaikimisi host: {host}\nVaikeport: {port}\nVaikimisi kasutajanimi: {username}", + "description": "Vaikimisi host: {host}\nVaikimisi kasutajanimi: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/fr.json b/homeassistant/components/netgear/translations/fr.json index f4b0af9789675..3230b8df45f36 100644 --- a/homeassistant/components/netgear/translations/fr.json +++ b/homeassistant/components/netgear/translations/fr.json @@ -15,7 +15,7 @@ "ssl": "Utilise un certificat SSL", "username": "Nom d'utilisateur (Optional)" }, - "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\n Port par d\u00e9faut\u00a0: {port}\n Nom d'utilisateur par d\u00e9faut\u00a0: {username}", + "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\nNom d'utilisateur par d\u00e9faut\u00a0: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json index 64452c9ef58d2..cf8d31ec3891d 100644 --- a/homeassistant/components/netgear/translations/hu.json +++ b/homeassistant/components/netgear/translations/hu.json @@ -15,7 +15,7 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v (nem k\u00f6telez\u0151)" }, - "description": "Alap\u00e9rtelmezett c\u00edm: {host}\nAlap\u00e9rtelmezett port: {port}\nAlap\u00e9rtelmezett felhaszn\u00e1l\u00f3n\u00e9v: {username}", + "description": "Alap\u00e9rtelmezett c\u00edm: {host}\nAlap\u00e9rtelmezett felhaszn\u00e1l\u00f3n\u00e9v: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/id.json b/homeassistant/components/netgear/translations/id.json index 6292d3c18dc7e..7f3eb3a07969b 100644 --- a/homeassistant/components/netgear/translations/id.json +++ b/homeassistant/components/netgear/translations/id.json @@ -15,7 +15,7 @@ "ssl": "Menggunakan sertifikat SSL", "username": "Nama Pengguna (Opsional)" }, - "description": "Host default: {host}\nPort default: {port}\nNama pengguna default: {username}", + "description": "Host default: {host}\nNama pengguna default: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/it.json b/homeassistant/components/netgear/translations/it.json index 8235fcc0ce976..15237e3a16a63 100644 --- a/homeassistant/components/netgear/translations/it.json +++ b/homeassistant/components/netgear/translations/it.json @@ -15,7 +15,7 @@ "ssl": "Utilizza un certificato SSL", "username": "Nome utente (Facoltativo)" }, - "description": "Host predefinito: {host}\nPorta predefinita: {port}\nNome utente predefinito: {username}", + "description": "Host predefinito: {host}\nNome utente predefinito: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/nb.json b/homeassistant/components/netgear/translations/nb.json new file mode 100644 index 0000000000000..371cb62d9b4be --- /dev/null +++ b/homeassistant/components/netgear/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn (Valgfritt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json index 22ac348af4e73..7333a0cd0eedd 100644 --- a/homeassistant/components/netgear/translations/nl.json +++ b/homeassistant/components/netgear/translations/nl.json @@ -15,7 +15,7 @@ "ssl": "Gebruikt een SSL certificaat", "username": "Gebruikersnaam (optioneel)" }, - "description": "Standaard host: {host}\nStandaard poort: {port}\nStandaard gebruikersnaam: {username}", + "description": "Standaardhost: {host}\n Standaard gebruikersnaam: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/no.json b/homeassistant/components/netgear/translations/no.json index 52020ae382496..73735f3e91a90 100644 --- a/homeassistant/components/netgear/translations/no.json +++ b/homeassistant/components/netgear/translations/no.json @@ -15,7 +15,7 @@ "ssl": "Bruker et SSL-sertifikat", "username": "Brukernavn (Valgfritt)" }, - "description": "Standard vert: {host}\nStandardport: {port}\nStandard brukernavn: {username}", + "description": "Standard vert: {host}\n Standard brukernavn: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json index f5feb67cfe116..0f3a6ad3cde6d 100644 --- a/homeassistant/components/netgear/translations/pl.json +++ b/homeassistant/components/netgear/translations/pl.json @@ -15,7 +15,7 @@ "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika (opcjonalnie)" }, - "description": "Domy\u015blne IP lub nazwa hosta: {host}\nDomy\u015blny port: {port}\nDomy\u015blna nazwa u\u017cytkownika: {username}", + "description": "Domy\u015blne IP lub nazwa hosta: {host}\nDomy\u015blna nazwa u\u017cytkownika: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/pt-BR.json b/homeassistant/components/netgear/translations/pt-BR.json index ec18c9a65dff7..66ffd692374a1 100644 --- a/homeassistant/components/netgear/translations/pt-BR.json +++ b/homeassistant/components/netgear/translations/pt-BR.json @@ -1,18 +1,21 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "config": "Erro de conex\u00e3o ou de login: verifique sua configura\u00e7\u00e3o" }, "step": { "user": { "data": { - "host": "Host (Opcional)", + "host": "Nome do host (Opcional)", "password": "Senha", "port": "Porta (Opcional)", - "ssl": "Utilize um certificado SSL", + "ssl": "Usar um certificado SSL", "username": "Usu\u00e1rio (Opcional)" }, - "description": "Host padr\u00e3o: {host}\n Porta padr\u00e3o: {port}\n Usu\u00e1rio padr\u00e3o: {username}", + "description": "Host padr\u00e3o: {host}\n Nome de usu\u00e1rio padr\u00e3o: {username}", "title": "Netgear" } } @@ -20,6 +23,9 @@ "options": { "step": { "init": { + "data": { + "consider_home": "Considere o tempo de casa (segundos)" + }, "description": "Especifique configura\u00e7\u00f5es opcionais", "title": "Netgear" } diff --git a/homeassistant/components/netgear/translations/ru.json b/homeassistant/components/netgear/translations/ru.json index 035492a01fe90..e6e5f9a008a64 100644 --- a/homeassistant/components/netgear/translations/ru.json +++ b/homeassistant/components/netgear/translations/ru.json @@ -15,7 +15,7 @@ "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 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0425\u043e\u0441\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {host}\n\u041f\u043e\u0440\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {port}\n\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {username}", + "description": "\u0425\u043e\u0441\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {host}\n\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/sk.json b/homeassistant/components/netgear/translations/sk.json new file mode 100644 index 0000000000000..ea85ad39b4ad7 --- /dev/null +++ b/homeassistant/components/netgear/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port (volite\u013en\u00fd)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/tr.json b/homeassistant/components/netgear/translations/tr.json index 07066485015cc..b29a9f075aa48 100644 --- a/homeassistant/components/netgear/translations/tr.json +++ b/homeassistant/components/netgear/translations/tr.json @@ -15,7 +15,7 @@ "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131 (\u0130ste\u011fe ba\u011fl\u0131)" }, - "description": "Varsay\u0131lan ana bilgisayar: {host}\n Varsay\u0131lan ba\u011flant\u0131 noktas\u0131: {port}\n Varsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}", + "description": "Varsay\u0131lan sunucu: {host}\nVarsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear/translations/zh-Hans.json b/homeassistant/components/netgear/translations/zh-Hans.json new file mode 100644 index 0000000000000..dd7b165d2d4cd --- /dev/null +++ b/homeassistant/components/netgear/translations/zh-Hans.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" + }, + "error": { + "config": "\u8fde\u63a5\u9519\u8bef\uff1a\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u662f\u5426\u6b63\u786e" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a\u5730\u5740 (\u53ef\u9009)", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3 (\u53ef\u9009)", + "ssl": "\u4f7f\u7528 SSL \u51ed\u8bc1\u767b\u5f55", + "username": "\u7528\u6237\u540d (\u53ef\u9009)" + }, + "description": "\u9ed8\u8ba4\u4e3b\u673a\u5730\u5740: {host}\n\u9ed8\u8ba4\u7528\u6237\u540d: {username}", + "title": "\u7f51\u4ef6\u8def\u7531\u5668" + } + } + }, + "options": { + "step": { + "init": { + "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e", + "title": "\u7f51\u4ef6\u8def\u7531\u5668" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/zh-Hant.json b/homeassistant/components/netgear/translations/zh-Hant.json index a4978fbb6bc2c..39b481f85b9e1 100644 --- a/homeassistant/components/netgear/translations/zh-Hant.json +++ b/homeassistant/components/netgear/translations/zh-Hant.json @@ -15,7 +15,7 @@ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", "username": "\u4f7f\u7528\u8005\u540d\u7a31\uff08\u9078\u9805\uff09" }, - "description": "\u9810\u8a2d\u4e3b\u6a5f\u7aef\uff1a{host}\n\u9810\u8a2d\u901a\u8a0a\u57e0\uff1a{port}\n\u9810\u8a2d\u4f7f\u7528\u8005\u540d\u7a31\uff1a{username}", + "description": "\u9810\u8a2d\u4e3b\u6a5f\u7aef\uff1a{host}\n\u9810\u8a2d\u4f7f\u7528\u8005\u540d\u7a31\uff1a{username}", "title": "Netgear" } } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index c02393e0f54cf..9b583739c8808 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.12"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["eternalegypt"] } diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index de607f2a3c0c9..c27c4f43920c5 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -76,7 +76,7 @@ class UsageSensor(LTESensor): @property def native_value(self): """Return the state of the sensor.""" - return round(self.modem_data.data.usage / 1024 ** 2, 1) + return round(self.modem_data.data.usage / 1024**2, 1) class GenericSensor(LTESensor): diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index a46acb46dc623..1d49293169e9d 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": ["neurio==0.3.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["neurio"] } diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index f605b32528ed2..29b80fb00e90f 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -11,5 +11,6 @@ "macaddress": "000231*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["nexia"] } diff --git a/homeassistant/components/nexia/translations/el.json b/homeassistant/components/nexia/translations/el.json new file mode 100644 index 0000000000000..47d2a624cd398 --- /dev/null +++ b/homeassistant/components/nexia/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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "brand": "\u039c\u03ac\u03c1\u03ba\u03b1", + "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" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/pt-BR.json b/homeassistant/components/nexia/translations/pt-BR.json index 932b4b8a72e0a..e9cce64aef968 100644 --- a/homeassistant/components/nexia/translations/pt-BR.json +++ b/homeassistant/components/nexia/translations/pt-BR.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { + "brand": "Marca", + "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "title": "Conecte-se a mynexia.com" } } } diff --git a/homeassistant/components/nexia/translations/sk.json b/homeassistant/components/nexia/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/nexia/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 3343e24b27766..c441f37078f3b 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "codeowners": ["@vividboarder"], "requirements": ["py_nextbusnext==0.1.5"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["py_nextbus"] } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index c1dea03aa0990..75163f3a92f77 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -5,5 +5,6 @@ "requirements": ["notifications-android-tv==0.1.3"], "codeowners": ["@tkdrob"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["notifications_android_tv"] } diff --git a/homeassistant/components/nfandroidtv/translations/el.json b/homeassistant/components/nfandroidtv/translations/el.json new file mode 100644 index 0000000000000..b95ec27a3bf83 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/el.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "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" + }, + "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 Android TV. \n\n \u0393\u03b9\u03b1 Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n \u0393\u03b9\u03b1 Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 (\u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2) \u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03ac\u03bd \u03cc\u03c7\u03b9, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b5\u03af \u03c4\u03b5\u03bb\u03b9\u03ba\u03ac \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7.", + "title": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/pt-BR.json b/homeassistant/components/nfandroidtv/translations/pt-BR.json new file mode 100644 index 0000000000000..43b7a296c2156 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Essa integra\u00e7\u00e3o requer as Notifica\u00e7\u00f5es para o aplicativo Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nVoc\u00ea deve configurar a reserva DHCP no roteador (consulte o manual do usu\u00e1rio do roteador) ou um endere\u00e7o IP est\u00e1tico no dispositivo. Se n\u00e3o, o dispositivo acabar\u00e1 por ficar indispon\u00edvel.", + "title": "Notifica\u00e7\u00f5es para Android TV / Fire TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/sk.json b/homeassistant/components/nfandroidtv/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json index 49cb077dc7956..c61b4f0cf9352 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -6,5 +6,6 @@ "requirements": ["py-nightscout==1.2.2"], "codeowners": ["@marciogranzotto"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["py_nightscout"] } diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json index 42762bfddce8c..aaa8db2b70df8 100644 --- a/homeassistant/components/nightscout/translations/el.json +++ b/homeassistant/components/nightscout/translations/el.json @@ -1,9 +1,19 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "Nightscout", "step": { "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" }, "description": "- \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 nightcout. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n - \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03b5\u03c4\u03b1\u03b9 (auth_default_roles! = readable).", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Nightscout." diff --git a/homeassistant/components/nightscout/translations/pt-BR.json b/homeassistant/components/nightscout/translations/pt-BR.json index 68dc0756725ba..4c31da855347f 100644 --- a/homeassistant/components/nightscout/translations/pt-BR.json +++ b/homeassistant/components/nightscout/translations/pt-BR.json @@ -5,13 +5,18 @@ }, "error": { "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "Chave da API", "url": "URL" - } + }, + "description": "- URL: o endere\u00e7o da sua inst\u00e2ncia nightscout. ou seja: https://myhomeassistant.duckdns.org:5423\n- Chave da API (opcional): Use somente se sua inst\u00e2ncia estiver protegida (auth_default_roles != readable).", + "title": "Insira as informa\u00e7\u00f5es do seu servidor Nightscout." } } } diff --git a/homeassistant/components/nightscout/translations/sk.json b/homeassistant/components/nightscout/translations/sk.json new file mode 100644 index 0000000000000..ff85312780312 --- /dev/null +++ b/homeassistant/components/nightscout/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index bb015a059b9d4..5057013bd5044 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": ["niko-home-control==0.2.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["nikohomecontrol"] } diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index bdc9220994798..cbb8db87e3274 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": ["niluclient==0.1.2"], "codeowners": ["@hfurubotten"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["niluclient"] } diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 11b1b3e3fdd45..7c45d193bbccf 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -4,11 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nina", "requirements": [ - "pynina==0.1.4" + "pynina==0.1.7" ], "dependencies": [], "codeowners": [ "@DeerMaximum" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pynina"] } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/el.json b/homeassistant/components/nina/translations/el.json index 181389950b999..9faf567662ac3 100644 --- a/homeassistant/components/nina/translations/el.json +++ b/homeassistant/components/nina/translations/el.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "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": { - "no_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03af\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03af\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json new file mode 100644 index 0000000000000..c22d3e065302a --- /dev/null +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "no_selection": "Selecione pelo menos uma cidade/condado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "Cidade/munic\u00edpio (M-Q)", + "_r_to_u": "Cidade/munic\u00edpio (R-U)", + "_v_to_z": "Cidade/munic\u00edpio (V-Z)", + "corona_filter": "Remover avisos do corona", + "slots": "M\u00e1ximo de avisos por cidade/munic\u00edpio" + }, + "title": "Selecione a cidade/munic\u00edpio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json index 6ab597dbef1af..0ba4436722d46 100644 --- a/homeassistant/components/nina/translations/zh-Hant.json +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 42169105930a8..87c29013544f8 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": ["pycarwings2==2.13"], "codeowners": ["@filcole"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pycarwings2"] } diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index e17270a62a0a3..6e7a9cbee5398 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -10,5 +10,6 @@ ], "codeowners": [], "iot_class": "local_polling", - "config_flow": true + "config_flow": true, + "loggers": ["nmap"] } diff --git a/homeassistant/components/nmap_tracker/translations/el.json b/homeassistant/components/nmap_tracker/translations/el.json index 74a0f8b1c9c10..202bc8e07221d 100644 --- a/homeassistant/components/nmap_tracker/translations/el.json +++ b/homeassistant/components/nmap_tracker/translations/el.json @@ -1,11 +1,41 @@ { + "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" + }, + "error": { + "invalid_hosts": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03b9 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03af \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2" + }, + "step": { + "user": { + "data": { + "exclude": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1) \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03af\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "home_interval": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03c3\u03b1\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd (\u03b4\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2)", + "hosts": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1) \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "scan_options": "\u0391\u03ba\u03b1\u03c4\u03ad\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Nmap" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Nmap. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03b9\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP (192.168.1.1), \u0394\u03af\u03ba\u03c4\u03c5\u03b1 IP (192.168.0.0/24) \u03ae \u0395\u03cd\u03c1\u03bf\u03c2 IP (192.168.1.0-32)." + } + } + }, "options": { + "error": { + "invalid_hosts": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03b9 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03af \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2" + }, "step": { "init": { "data": { - "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03b1\u03bd\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c9\u03c2 \u03cc\u03c7\u03b9 \u03c3\u03c0\u03af\u03c4\u03b9, \u03b1\u03c6\u03bf\u03cd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03c3\u03c4\u03b5\u03af." - } + "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03b1\u03bd\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c9\u03c2 \u03cc\u03c7\u03b9 \u03c3\u03c0\u03af\u03c4\u03b9, \u03b1\u03c6\u03bf\u03cd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03c3\u03c4\u03b5\u03af.", + "exclude": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1) \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03af\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "home_interval": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03c3\u03b1\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd (\u03b4\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2)", + "hosts": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1) \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", + "interval_seconds": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2", + "scan_options": "\u0391\u03ba\u03b1\u03c4\u03ad\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Nmap", + "track_new_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bd\u03ad\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Nmap. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03b9\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP (192.168.1.1), \u0394\u03af\u03ba\u03c4\u03c5\u03b1 IP (192.168.0.0/24) \u03ae \u0395\u03cd\u03c1\u03bf\u03c2 IP (192.168.1.0-32)." } } - } + }, + "title": "\u0399\u03c7\u03bd\u03b7\u03bb\u03ac\u03c4\u03b7\u03c2 Nmap" } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/pt-BR.json b/homeassistant/components/nmap_tracker/translations/pt-BR.json new file mode 100644 index 0000000000000..bf058495cb934 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_hosts": "Hosts inv\u00e1lidos" + }, + "step": { + "user": { + "data": { + "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir do escaneamento", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneamento de dispositivos ativos (preservar bateria)", + "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para escanear", + "scan_options": "Op\u00e7\u00f5es de escaneamento bruto configur\u00e1veis para Nmap" + }, + "description": "Configure os hosts a serem verificados pelo Nmap. O endere\u00e7o de rede e as exclus\u00f5es podem ser endere\u00e7os IP (192.168.1.1), redes IP (192.168.0.0/24) ou intervalos de IP (192.168.1.0-32)." + } + } + }, + "options": { + "error": { + "invalid_hosts": "Hosts inv\u00e1lidos" + }, + "step": { + "init": { + "data": { + "consider_home": "Segundos para esperar at\u00e9 marcar um rastreador de dispositivo como fora de casa depois de n\u00e3o ser visto.", + "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir do escaneamento", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneamento de dispositivos ativos (preservar bateria)", + "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para escanear", + "interval_seconds": "Intervalo de varredura", + "scan_options": "Op\u00e7\u00f5es de varredura configur\u00e1veis brutas para Nmap", + "track_new_devices": "Rastrear novos dispositivos" + }, + "description": "Configure hosts a serem digitalizados pelo Nmap. O endere\u00e7o de rede e exclus\u00f5es podem ser Endere\u00e7os IP (192.168.1.1), Redes IP (192.168.0.0/24) ou Faixas IP (192.168.1.0-32)." + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index 82723f9792433..0c97b08f6800a 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": ["pyrail==0.0.3"], "codeowners": ["@thibmaek"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyrail"] } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 8ad99c8a5c22e..618110051b6a4 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": ["noaa-coops==0.1.8"], "codeowners": ["@jdelaney72"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["noaa_coops"] } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 146f4b2ff27ed..b6182d7ed843c 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -106,31 +106,31 @@ def name(self) -> str: """Return the name of the sensor.""" return self._name - @property # type: ignore + @property # type: ignore[misc] @round_state def air_quality_index(self): """Return the Air Quality Index (AQI).""" return self._api.data.get("aqi") - @property # type: ignore + @property # type: ignore[misc] @round_state def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" return self._api.data.get("no2_concentration") - @property # type: ignore + @property # type: ignore[misc] @round_state def ozone(self): """Return the O3 (ozone) level.""" return self._api.data.get("o3_concentration") - @property # type: ignore + @property # type: ignore[misc] @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" return self._api.data.get("pm25_concentration") - @property # type: ignore + @property # type: ignore[misc] @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index ade1a14959009..81572fe9cb7e8 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/norway_air", "requirements": ["pyMetno==0.9.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["metno"] } diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 5f26c952b31d5..af29a9fba99f9 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -178,7 +178,7 @@ class BaseNotificationService: # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - hass: HomeAssistant = None # type: ignore + hass: HomeAssistant = None # type: ignore[assignment] # Name => target registered_targets: dict[str, str] @@ -246,7 +246,7 @@ async def async_register_services(self) -> None: if hasattr(self, "targets"): stale_targets = set(self.registered_targets) - for name, target in self.targets.items(): # type: ignore + for name, target in self.targets.items(): # type: ignore[attr-defined] 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/notify_events/manifest.json b/homeassistant/components/notify_events/manifest.json index 96eda381506ac..5247e19698825 100644 --- a/homeassistant/components/notify_events/manifest.json +++ b/homeassistant/components/notify_events/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/notify_events", "codeowners": ["@matrozov", "@papajojo"], "requirements": ["notify-events==1.0.4"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["notify_events"] } diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index cdaab389dc7a3..c9e59107c1b82 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER @@ -74,7 +73,7 @@ async def _async_verify(self, step_id: str, schema: vol.Schema) -> FlowResult: return self.async_create_entry(title=self._username, data=data) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 378d6442e3157..fa19ef81c8c4a 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": ["aionotion==3.0.2"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aionotion"] } diff --git a/homeassistant/components/notion/translations/el.json b/homeassistant/components/notion/translations/el.json new file mode 100644 index 0000000000000..ee39e67ae73d1 --- /dev/null +++ b/homeassistant/components/notion/translations/el.json @@ -0,0 +1,29 @@ +{ + "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", + "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_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", + "no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username}.", + "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": { + "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" + }, + "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pt-BR.json b/homeassistant/components/notion/translations/pt-BR.json index 084048a625a20..d778a301ee1e8 100644 --- a/homeassistant/components/notion/translations/pt-BR.json +++ b/homeassistant/components/notion/translations/pt-BR.json @@ -1,13 +1,26 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, "error": { - "no_devices": "Nenhum dispositivo encontrado na conta" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices": "Nenhum dispositivo encontrado na conta", + "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Por favor, digite novamente a senha para {username}.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", - "username": "Usu\u00e1rio/ende\u00e7o de e-mail" + "username": "Usu\u00e1rio" }, "title": "Preencha suas informa\u00e7\u00f5es" } diff --git a/homeassistant/components/notion/translations/sk.json b/homeassistant/components/notion/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/notion/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index dfc6ad62d9075..a9f8f64da06f7 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": ["nsw-fuel-api-client==1.1.0"], "codeowners": ["@nickw444"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["nsw_fuel"] } diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index ce75e72f5de87..694089b13965b 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aio_geojson_nsw_rfs_incidents"] } diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index d2dbb12ebc536..aea63a692a5cd 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -11,5 +11,6 @@ "macaddress": "002338*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["nuheat"] } diff --git a/homeassistant/components/nuheat/translations/el.json b/homeassistant/components/nuheat/translations/el.json index 1d2ee7844f9f7..9e6d0193fae4a 100644 --- a/homeassistant/components/nuheat/translations/el.json +++ b/homeassistant/components/nuheat/translations/el.json @@ -1,7 +1,24 @@ { "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": { - "invalid_thermostat": "\u039f \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\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", + "invalid_thermostat": "\u039f \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "serial_number": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7.", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03b7\u03c4\u03b9\u03ba\u03cc \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03ae \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03c3\u03b1\u03c2, \u03c3\u03c5\u03bd\u03b4\u03b5\u03cc\u03bc\u03b5\u03bd\u03bf\u03b9 \u03c3\u03c4\u03bf https://MyNuHeat.com \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf\u03bd/\u03c4\u03bf\u03c5\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b5\u03c2 \u03c3\u03b1\u03c2.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf NuHeat" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/pt-BR.json b/homeassistant/components/nuheat/translations/pt-BR.json index 7963212e49caf..e90f8e1cfe9ce 100644 --- a/homeassistant/components/nuheat/translations/pt-BR.json +++ b/homeassistant/components/nuheat/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O termostato j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_thermostat": "O n\u00famero de s\u00e9rie do termostato \u00e9 inv\u00e1lido.", "unknown": "Erro inesperado" @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "password": "Senha", "serial_number": "N\u00famero de s\u00e9rie do termostato.", "username": "Usu\u00e1rio" }, diff --git a/homeassistant/components/nuheat/translations/sk.json b/homeassistant/components/nuheat/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/nuheat/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 8642423fd8d63..8a9b7c506b44e 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -10,5 +10,6 @@ "hostname": "nuki_bridge_*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pynuki"] } diff --git a/homeassistant/components/nuki/translations/el.json b/homeassistant/components/nuki/translations/el.json index 8e12bb837f4d5..1422144b0e6ac 100644 --- a/homeassistant/components/nuki/translations/el.json +++ b/homeassistant/components/nuki/translations/el.json @@ -1,8 +1,27 @@ { "config": { + "abort": { + "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", + "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": { "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Nuki \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 \u03bc\u03b5 \u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03ac \u03c3\u03b1\u03c2." + "data": { + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Nuki \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 \u03bc\u03b5 \u03c4\u03b7 \u03b3\u03ad\u03c6\u03c5\u03c1\u03ac \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" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } } } } diff --git a/homeassistant/components/nuki/translations/pt-BR.json b/homeassistant/components/nuki/translations/pt-BR.json new file mode 100644 index 0000000000000..e00d14b479e39 --- /dev/null +++ b/homeassistant/components/nuki/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "token": "Token de acesso" + }, + "description": "A integra\u00e7\u00e3o Nuki precisa se autenticar novamente com sua ponte.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta", + "token": "Token de acesso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/sk.json b/homeassistant/components/nuki/translations/sk.json new file mode 100644 index 0000000000000..16e7623680598 --- /dev/null +++ b/homeassistant/components/nuki/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "token": "Pr\u00edstupov\u00fd token" + } + }, + "user": { + "data": { + "port": "Port", + "token": "Pr\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index a65c4998554c6..0f02bd6b8f71c 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/numato", "requirements": ["numato-gpio==0.10.0"], "codeowners": ["@clssn"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["numato_gpio"] } diff --git a/homeassistant/components/number/translations/el.json b/homeassistant/components/number/translations/el.json new file mode 100644 index 0000000000000..0afb4c73e7df8 --- /dev/null +++ b/homeassistant/components/number/translations/el.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b3\u03b9\u03b1 {entity_name}" + } + }, + "title": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/pt-BR.json b/homeassistant/components/number/translations/pt-BR.json new file mode 100644 index 0000000000000..b45277027445a --- /dev/null +++ b/homeassistant/components/number/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Definir valor para {entity_name}" + } + }, + "title": "N\u00famero" +} \ No newline at end of file diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 6be66fe64c1b9..775c512f9461a 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -115,7 +115,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/nut/diagnostics.py b/homeassistant/components/nut/diagnostics.py new file mode 100644 index 0000000000000..e8c0a0711dc78 --- /dev/null +++ b/homeassistant/components/nut/diagnostics.py @@ -0,0 +1,68 @@ +"""Diagnostics support for Nut.""" +from __future__ import annotations + +from typing import Any + +import attr + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import PyNUTData +from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID + +TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} + hass_data = hass.data[DOMAIN][entry.entry_id] + + # Get information from Nut library + nut_data: PyNUTData = hass_data[PYNUT_DATA] + data["nut_data"] = {"ups_list": nut_data.ups_list, "status": nut_data.status} + + # Gather information how this Nut device is represented in Home Assistant + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + hass_device = device_registry.async_get_device( + identifiers={(DOMAIN, hass_data[PYNUT_UNIQUE_ID])} + ) + if not hass_device: + return data + + data["device"] = { + **attr.asdict(hass_device), + "entities": {}, + } + + hass_entities = er.async_entries_for_device( + entity_registry, + device_id=hass_device.id, + include_disabled_entities=True, + ) + + for entity_entry in hass_entities: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + # The entity_id is already provided at root level. + state_dict.pop("entity_id", None) + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) + + data["device"]["entities"][entity_entry.entity_id] = { + **attr.asdict( + entity_entry, filter=lambda attr, value: attr.name != "entity_id" + ), + "state": state_dict, + } + + return data diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 2489078ebd6d1..4a07713fa3054 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco", "@ollo69"], "config_flow": true, "zeroconf": ["_nut._tcp.local."], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pynut2"] } diff --git a/homeassistant/components/nut/translations/el.json b/homeassistant/components/nut/translations/el.json index f606163c1fa0c..971d6476a2376 100644 --- a/homeassistant/components/nut/translations/el.json +++ b/homeassistant/components/nut/translations/el.json @@ -1,5 +1,12 @@ { "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" + }, "step": { "resources": { "data": { @@ -7,16 +14,34 @@ }, "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03cc\u03c1\u03bf\u03c5\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" }, + "ups": { + "data": { + "alias": "\u03a8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf", + "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf UPS \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" + }, "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae NUT" } } }, "options": { + "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": { "init": { "data": { - "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9" + "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9", + "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": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03cc\u03c1\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd." } diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json index bb4d9f9907e5f..6b29911b28a3e 100644 --- a/homeassistant/components/nut/translations/it.json +++ b/homeassistant/components/nut/translations/it.json @@ -28,7 +28,7 @@ "port": "Porta", "username": "Nome utente" }, - "title": "Connessione al server NUT" + "title": "Connettiti al server NUT" } } }, diff --git a/homeassistant/components/nut/translations/pt-BR.json b/homeassistant/components/nut/translations/pt-BR.json index 8b6b7538ba8af..d6be7b4104493 100644 --- a/homeassistant/components/nut/translations/pt-BR.json +++ b/homeassistant/components/nut/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { @@ -20,15 +20,30 @@ "resources": "Recursos" }, "title": "Escolha o no-break (UPS) para monitorar" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se ao servidor NUT" } } }, "options": { + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, "step": { "init": { "data": { + "resources": "Recursos", "scan_interval": "Intervalo de escaneamento (segundos)" - } + }, + "description": "Escolha os recursos dos sensores." } } } diff --git a/homeassistant/components/nut/translations/sk.json b/homeassistant/components/nut/translations/sk.json new file mode 100644 index 0000000000000..434ad4a26b20d --- /dev/null +++ b/homeassistant/components/nut/translations/sk.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "resources": { + "data": { + "resources": "Prostriedky" + }, + "title": "Vyberte prostriedky, ktor\u00e9 chcete monitorova\u0165" + }, + "ups": { + "data": { + "alias": "Alias", + "resources": "Prostriedky" + }, + "title": "Vyberte UPS, ktor\u00fa chcete monitorova\u0165" + }, + "user": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "title": "Pripoji\u0165 k serveru NUT" + } + } + }, + "options": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "init": { + "data": { + "resources": "Prostriedky", + "scan_interval": "Skenovac\u00ed interval (v sekund\u00e1ch)" + }, + "description": "Vyberte senzory." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 2e6f58028e011..091bdaa573642 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pynws==1.3.2"], "quality_scale": "platinum", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["metar", "pynws"] } diff --git a/homeassistant/components/nws/translations/el.json b/homeassistant/components/nws/translations/el.json index 414d71d2d52a4..14a404a8c661f 100644 --- a/homeassistant/components/nws/translations/el.json +++ b/homeassistant/components/nws/translations/el.json @@ -1,8 +1,18 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "user": { "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "station": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd METAR" }, "description": "\u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd METAR, \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03ae\u03ba\u03bf\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03bb\u03b7\u03c3\u03b9\u03ad\u03c3\u03c4\u03b5\u03c1\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd. \u03a0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd, \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5.", diff --git a/homeassistant/components/nws/translations/pt-BR.json b/homeassistant/components/nws/translations/pt-BR.json index 3d168bcce30b6..2e74dcad77e7b 100644 --- a/homeassistant/components/nws/translations/pt-BR.json +++ b/homeassistant/components/nws/translations/pt-BR.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "api_key": "Chave da API", "latitude": "Latitude", "longitude": "Longitude", "station": "C\u00f3digo da esta\u00e7\u00e3o METAR" diff --git a/homeassistant/components/nws/translations/sk.json b/homeassistant/components/nws/translations/sk.json new file mode 100644 index 0000000000000..abb3969f6b4ac --- /dev/null +++ b/homeassistant/components/nws/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 2aa3df8d167f2..9f826a4c4b263 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": ["pynx584==0.5"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["nx584"] } diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index 8aa18502ba35e..c7a1699a86cf8 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -19,7 +19,6 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_NAME, @@ -65,7 +64,7 @@ def async_get_options_flow(config_entry): return NZBGetOptionsFlowHandler(config_entry) async def async_step_import( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 951d5237736be..6d4ea286317b2 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pynzbgetapi==0.2.0"], "codeowners": ["@chriscla"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pynzbgetapi"] } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 42cacfc8ab5f9..9e5bd6e4ac957 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -126,7 +126,7 @@ def native_value(self): if "DownloadRate" in sensor_type and value > 0: # Convert download rate from Bytes/s to MBytes/s - return round(value / 2 ** 20, 2) + return round(value / 2**20, 2) if "UpTimeSec" in sensor_type and value > 0: uptime = utcnow() - timedelta(seconds=value) diff --git a/homeassistant/components/nzbget/translations/el.json b/homeassistant/components/nzbget/translations/el.json index 394e8ca41efcf..c2d086e8be1e7 100644 --- a/homeassistant/components/nzbget/translations/el.json +++ b/homeassistant/components/nzbget/translations/el.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "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": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { @@ -9,6 +10,15 @@ "flow_title": "NZBGet: {\u03cc\u03bd\u03bf\u03bc\u03b1}", "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" + }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf NZBGet" } } diff --git a/homeassistant/components/nzbget/translations/pt-BR.json b/homeassistant/components/nzbget/translations/pt-BR.json new file mode 100644 index 0000000000000..69c0c575ecaf0 --- /dev/null +++ b/homeassistant/components/nzbget/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "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" + }, + "title": "Conecte-se ao NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/sk.json b/homeassistant/components/nzbget/translations/sk.json new file mode 100644 index 0000000000000..39d2e182c40be --- /dev/null +++ b/homeassistant/components/nzbget/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/uk.json b/homeassistant/components/nzbget/translations/uk.json index eba15cca19c17..a7f7e8b3f07df 100644 --- a/homeassistant/components/nzbget/translations/uk.json +++ b/homeassistant/components/nzbget/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "error": { diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json index 28edec03d67c6..6040b52b67037 100644 --- a/homeassistant/components/nzbget/translations/zh-Hant.json +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index a1d672ba59518..b1b203a8a61ea 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": ["oasatelematics==0.3"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["oasatelematics"] } diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 05121c81ac73c..f908ad161793c 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": ["pyobihai==1.3.1"], "codeowners": ["@dshokouhi"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyobihai"] } diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 6c1eb62831c87..eded3bec16a4a 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring OctoPrint 3D printers.""" +from __future__ import annotations + from datetime import timedelta import logging from typing import cast @@ -52,7 +54,7 @@ def ensure_valid_path(value): return value -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] DEFAULT_NAME = "OctoPrint" CONF_NUMBER_OF_TOOLS = "number_of_tools" CONF_BED = "bed" @@ -175,7 +177,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = {"coordinator": coordinator, "client": client} + hass.data[DOMAIN][entry.entry_id] = { + "coordinator": coordinator, + "client": client, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py new file mode 100644 index 0000000000000..97676592f4753 --- /dev/null +++ b/homeassistant/components/octoprint/button.py @@ -0,0 +1,133 @@ +"""Support for Octoprint buttons.""" +from pyoctoprintapi import OctoprintClient, OctoprintPrinterInfo + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import OctoprintDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Octoprint control buttons.""" + coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ]["coordinator"] + client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"] + device_id = config_entry.unique_id + assert device_id is not None + + async_add_entities( + [ + OctoprintResumeJobButton(coordinator, device_id, client), + OctoprintPauseJobButton(coordinator, device_id, client), + OctoprintStopJobButton(coordinator, device_id, client), + ] + ) + + +class OctoprintButton(CoordinatorEntity, ButtonEntity): + """Represent an OctoPrint binary sensor.""" + + coordinator: OctoprintDataUpdateCoordinator + client: OctoprintClient + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + button_type: str, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator) + self.client = client + self._device_id = device_id + self._attr_name = f"OctoPrint {button_type}" + self._attr_unique_id = f"{button_type}-{device_id}" + + @property + def device_info(self): + """Device info.""" + return self.coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self.coordinator.data["printer"] + + +class OctoprintPauseJobButton(OctoprintButton): + """Pause the active job.""" + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Pause Job", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + printer: OctoprintPrinterInfo = self.coordinator.data["printer"] + + if printer.state.flags.printing: + await self.client.pause_job() + elif not printer.state.flags.paused and not printer.state.flags.pausing: + raise InvalidPrinterState("Printer is not printing") + + +class OctoprintResumeJobButton(OctoprintButton): + """Resume the active job.""" + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Resume Job", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + printer: OctoprintPrinterInfo = self.coordinator.data["printer"] + + if printer.state.flags.paused: + await self.client.resume_job() + elif not printer.state.flags.printing and not printer.state.flags.resuming: + raise InvalidPrinterState("Printer is not currently paused") + + +class OctoprintStopJobButton(OctoprintButton): + """Resume the active job.""" + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Stop Job", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + printer: OctoprintPrinterInfo = self.coordinator.data["printer"] + + if printer.state.flags.printing or printer.state.flags.paused: + await self.client.cancel_job() + + +class InvalidPrinterState(HomeAssistantError): + """Service attempted in invalid state.""" diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index c2f0f96b6d585..1bd54e2214eea 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -145,9 +145,15 @@ async def async_step_zeroconf( await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() - self.context["title_placeholders"] = { - CONF_HOST: discovery_info.host, - } + self.context.update( + { + "title_placeholders": {CONF_HOST: discovery_info.host}, + "configuration_url": ( + f"http://{discovery_info.host}:{discovery_info.port}" + f"{discovery_info.properties[CONF_PATH]}" + ), + } + ) self.discovery_schema = _schema_with_defaults( host=discovery_info.host, @@ -166,9 +172,12 @@ async def async_step_ssdp( self._abort_if_unique_id_configured() url = URL(discovery_info.upnp["presentationURL"]) - self.context["title_placeholders"] = { - CONF_HOST: url.host, - } + self.context.update( + { + "title_placeholders": {CONF_HOST: url.host}, + "configuration_url": discovery_info.upnp["presentationURL"], + } + ) self.discovery_schema = _schema_with_defaults( host=url.host, diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index e150fbe5c57e8..385ab88428a19 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -12,5 +12,6 @@ "deviceType": "urn:schemas-upnp-org:device:Basic:1" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyoctoprintapi"] } diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 8057de5596668..5a094c10987c5 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -13,7 +13,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -47,13 +47,23 @@ async def async_setup_entry( assert device_id is not None - entities: list[SensorEntity] = [] - if coordinator.data["printer"]: - printer_info = coordinator.data["printer"] - types = ["actual", "target"] - for tool in printer_info.temperatures: - for temp_type in types: - entities.append( + known_tools = set() + + @callback + def async_add_tool_sensors() -> None: + if not coordinator.data["printer"]: + return + + new_tools = [] + for tool in [ + tool + for tool in coordinator.data["printer"].temperatures + if tool.name not in known_tools + ]: + assert device_id is not None + known_tools.add(tool.name) + for temp_type in ("actual", "target"): + new_tools.append( OctoPrintTemperatureSensor( coordinator, tool.name, @@ -61,13 +71,20 @@ async def async_setup_entry( device_id, ) ) - else: - _LOGGER.debug("Printer appears to be offline, skipping temperature sensors") + if new_tools: + async_add_entities(new_tools) + + config_entry.async_on_unload(coordinator.async_add_listener(async_add_tool_sensors)) - entities.append(OctoPrintStatusSensor(coordinator, device_id)) - entities.append(OctoPrintJobPercentageSensor(coordinator, device_id)) - entities.append(OctoPrintEstimatedFinishTimeSensor(coordinator, device_id)) - entities.append(OctoPrintStartTimeSensor(coordinator, device_id)) + if coordinator.data["printer"]: + async_add_tool_sensors() + + entities: list[SensorEntity] = [ + OctoPrintStatusSensor(coordinator, device_id), + OctoPrintJobPercentageSensor(coordinator, device_id), + OctoPrintEstimatedFinishTimeSensor(coordinator, device_id), + OctoPrintStartTimeSensor(coordinator, device_id), + ] async_add_entities(entities) diff --git a/homeassistant/components/octoprint/translations/cs.json b/homeassistant/components/octoprint/translations/cs.json index 4134a04508f52..fa519dfe6e1e6 100644 --- a/homeassistant/components/octoprint/translations/cs.json +++ b/homeassistant/components/octoprint/translations/cs.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/octoprint/translations/el.json b/homeassistant/components/octoprint/translations/el.json new file mode 100644 index 0000000000000..60dc47229e2df --- /dev/null +++ b/homeassistant/components/octoprint/translations/el.json @@ -0,0 +1,30 @@ +{ + "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", + "auth_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd api \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", + "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" + }, + "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" + }, + "flow_title": "\u0395\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2 OctoPrint: {host}", + "progress": { + "get_api_key": "\u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03bf OctoPrint UI \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf 'Allow' \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf 'Home Assistant'." + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2", + "port": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2", + "ssl": "\u03a7\u03c1\u03ae\u03c3\u03b7 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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/nb.json b/homeassistant/components/octoprint/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/octoprint/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/pt-BR.json b/homeassistant/components/octoprint/translations/pt-BR.json new file mode 100644 index 0000000000000..f8af97f752625 --- /dev/null +++ b/homeassistant/components/octoprint/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "auth_failed": "Falha ao recuperar a chave de API do aplicativo", + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "Impressora OctoPrint: {host}", + "progress": { + "get_api_key": "Abra a interface do usu\u00e1rio do OctoPrint e clique em 'Permitir' na solicita\u00e7\u00e3o de acesso para 'Assistente dom\u00e9stico'." + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "path": "Caminho do aplicativo", + "port": "N\u00famero da porta", + "ssl": "Usar SSL", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index 29c2b1e7fa465..e289e7a2e1402 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": ["oemthermostat==1.1.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["oemthermostat"] } diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index ea2e951d08481..396bbb91a9c20 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/omnilogic", "requirements": ["omnilogic==0.4.5"], "codeowners": ["@oliver84", "@djtimca", "@gentoosu"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["config", "omnilogic"] } diff --git a/homeassistant/components/omnilogic/translations/pt-BR.json b/homeassistant/components/omnilogic/translations/pt-BR.json new file mode 100644 index 0000000000000..6a48db65df54b --- /dev/null +++ b/homeassistant/components/omnilogic/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ph_offset": "Deslocamento de pH (positivo ou negativo)", + "polling_interval": "Intervalo de sondagem (em segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/sk.json b/homeassistant/components/omnilogic/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/uk.json b/homeassistant/components/omnilogic/translations/uk.json index 21ebf6f4fafa2..6c65ff99dbbd3 100644 --- a/homeassistant/components/omnilogic/translations/uk.json +++ b/homeassistant/components/omnilogic/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index 89e49de710af9..0a25890fd8a26 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onboarding/translations/el.json b/homeassistant/components/onboarding/translations/el.json new file mode 100644 index 0000000000000..29d0c1225388e --- /dev/null +++ b/homeassistant/components/onboarding/translations/el.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u03a5\u03c0\u03bd\u03bf\u03b4\u03c9\u03bc\u03ac\u03c4\u03b9\u03bf", + "kitchen": "\u039a\u03bf\u03c5\u03b6\u03af\u03bd\u03b1", + "living_room": "\u03a3\u03b1\u03bb\u03cc\u03bd\u03b9" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 0d041463d112e..b277bd97edf87 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -188,9 +188,8 @@ async def post(self, request): await self._async_mark_done(hass) - await hass.config_entries.flow.async_init( - "met", context={"source": "onboarding"} - ) + # Integrations to set up when finishing onboarding + onboard_integrations = ["met", "radio_browser"] # pylint: disable=import-outside-toplevel from homeassistant.components import hassio @@ -199,9 +198,17 @@ async def post(self, request): hassio.is_hassio(hass) and "raspberrypi" in hassio.get_core_info(hass)["machine"] ): - await hass.config_entries.flow.async_init( - "rpi_power", context={"source": "onboarding"} + onboard_integrations.append("rpi_power") + + # Set up integrations after onboarding + await asyncio.gather( + *( + hass.config_entries.flow.async_init( + domain, context={"source": "onboarding"} + ) + for domain in onboard_integrations ) + ) return self.json({}) diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index cc450ada61ad3..190b5c790cc5a 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -9,5 +9,6 @@ "documentation": "https://www.home-assistant.io/integrations/oncue", "requirements": ["aiooncue==0.3.2"], "codeowners": ["@bdraco"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aiooncue"] } diff --git a/homeassistant/components/oncue/translations/el.json b/homeassistant/components/oncue/translations/el.json new file mode 100644 index 0000000000000..cdc7ae85736f1 --- /dev/null +++ b/homeassistant/components/oncue/translations/el.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oncue/translations/nb.json b/homeassistant/components/oncue/translations/nb.json new file mode 100644 index 0000000000000..ef1398553b590 --- /dev/null +++ b/homeassistant/components/oncue/translations/nb.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oncue/translations/pt-BR.json b/homeassistant/components/oncue/translations/pt-BR.json new file mode 100644 index 0000000000000..d86aef5d51d73 --- /dev/null +++ b/homeassistant/components/oncue/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oncue/translations/sk.json b/homeassistant/components/oncue/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/oncue/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index 4c3ee64779a35..d3d9c7d137613 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -6,5 +6,6 @@ "requirements": ["ondilo==0.2.0"], "dependencies": ["http"], "codeowners": ["@JeromeHXP"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["ondilo"] } diff --git a/homeassistant/components/ondilo_ico/translations/el.json b/homeassistant/components/ondilo_ico/translations/el.json new file mode 100644 index 0000000000000..ef89a2c81d987 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pt-BR.json b/homeassistant/components/ondilo_ico/translations/pt-BR.json new file mode 100644 index 0000000000000..c219c32f8e315 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/sk.json b/homeassistant/components/ondilo_ico/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index f48236c7f37ea..d7b301f9c2343 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], "codeowners": ["@garbled1", "@epenet"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pi1wire", "pyownet"] } diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json new file mode 100644 index 0000000000000..35b39838f5daf --- /dev/null +++ b/homeassistant/components/onewire/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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_path": "\u039f \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5." + }, + "step": { + "owserver": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03b5\u03c1\u03b5\u03b9\u03ce\u03bd owserver" + }, + "user": { + "data": { + "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json new file mode 100644 index 0000000000000..401452bcaf515 --- /dev/null +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_path": "Diret\u00f3rio n\u00e3o encontrado." + }, + "step": { + "owserver": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "title": "Definir detalhes do servidor" + }, + "user": { + "data": { + "type": "Tipo de conex\u00e3o" + }, + "title": "Configurar 1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sk.json b/homeassistant/components/onewire/translations/sk.json new file mode 100644 index 0000000000000..bd43098e5553c --- /dev/null +++ b/homeassistant/components/onewire/translations/sk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_path": "Adres\u00e1r sa nena\u0161iel." + }, + "step": { + "owserver": { + "data": { + "port": "Port" + }, + "title": "Nastavenie owserver" + }, + "user": { + "data": { + "type": "Typ pripojenia" + }, + "title": "Nastavenie 1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 39c1686d03e61..4f2dadde270bf 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": ["onkyo-eiscp==1.2.7"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["eiscp"] } diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index a7faa60cdcd4c..cd220500751d4 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["onvif", "wsdiscovery", "zeep"] } diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 9574d44edeae2..b518dbbb45179 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -1,10 +1,13 @@ """ONVIF event parsers.""" +from collections.abc import Callable, Coroutine +from typing import Any + from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry from .models import Event -PARSERS = Registry() +PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event]]] = Registry() @PARSERS.register("tns1:VideoSource/MotionAlarm") diff --git a/homeassistant/components/onvif/translations/el.json b/homeassistant/components/onvif/translations/el.json index 945b9cfa6a85f..8bc93c4a3d3a6 100644 --- a/homeassistant/components/onvif/translations/el.json +++ b/homeassistant/components/onvif/translations/el.json @@ -2,10 +2,14 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7", + "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_h264": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ae\u03c1\u03c7\u03b1\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c1\u03bf\u03ad\u03c2 H264. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2.", "no_mac": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ONVIF.", "onvif_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \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." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "auth": { "data": { @@ -14,6 +18,16 @@ }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, + "configure": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF" + }, "configure_profile": { "data": { "include": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2" @@ -30,11 +44,15 @@ "manual_input": { "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", "port": "\u0398\u03cd\u03c1\u03b1" }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF" }, "user": { + "data": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7" + }, "description": "\u039a\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 ONVIF \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03c5\u03bd \u03c4\u03bf Profile S. \n\n \u039f\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c3\u03ba\u03b5\u03c5\u03b1\u03c3\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03bf ONVIF \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf ONVIF \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03ac\u03c2 \u03c3\u03b1\u03c2.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF" } diff --git a/homeassistant/components/onvif/translations/nb.json b/homeassistant/components/onvif/translations/nb.json new file mode 100644 index 0000000000000..309e842841309 --- /dev/null +++ b/homeassistant/components/onvif/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "configure": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json index 3304203c57a76..d5586b2dd2b52 100644 --- a/homeassistant/components/onvif/translations/pt-BR.json +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "O dispositivo ONVIF j\u00e1 est\u00e1 configurado.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para dispositivos ONVIF 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_h264": "N\u00e3o h\u00e1 fluxos H264 dispon\u00edveis. Verifique a configura\u00e7\u00e3o do perfil no seu dispositivo.", "no_mac": "N\u00e3o foi poss\u00edvel configurar um ID \u00fanico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." }, + "error": { + "cannot_connect": "Falha ao conectar" + }, "step": { "auth": { "data": { @@ -15,6 +18,16 @@ }, "title": "Configurar autentica\u00e7\u00e3o" }, + "configure": { + "data": { + "host": "Nome do host", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar dispositivo ONVIF" + }, "configure_profile": { "data": { "include": "Criar entidade c\u00e2mera" @@ -30,13 +43,16 @@ }, "manual_input": { "data": { - "host": "Endere\u00e7o (IP)", + "host": "Nome do host", "name": "Nome", "port": "Porta" }, "title": "Configurar dispositivo ONVIF" }, "user": { + "data": { + "auto": "Pesquisar automaticamente" + }, "description": "Ao clicar em enviar, procuraremos na sua rede por dispositivos ONVIF compat\u00edveis com o Perfil S. \n\nAlguns fabricantes deixam o ONVIF desativado por padr\u00e3o. Verifique se o ONVIF est\u00e1 ativado na configura\u00e7\u00e3o da sua c\u00e2mera.", "title": "Configura\u00e7\u00e3o do dispositivo ONVIF" } diff --git a/homeassistant/components/onvif/translations/sk.json b/homeassistant/components/onvif/translations/sk.json new file mode 100644 index 0000000000000..f9a7d7d07bf97 --- /dev/null +++ b/homeassistant/components/onvif/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" + }, + "step": { + "auth": { + "data": { + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "configure": { + "data": { + "name": "N\u00e1zov", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "manual_input": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index 653ccff69804e..de42d19d8c959 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -28,8 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: open_meteo = OpenMeteo(session=session) async def async_update_forecast() -> Forecast: - zone = hass.states.get(entry.data[CONF_ZONE]) - if zone is None: + if (zone := hass.states.get(entry.data[CONF_ZONE])) is None: raise UpdateFailed(f"Zone '{entry.data[CONF_ZONE]}' not found") try: diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py index 2a7b292c363bf..7f63b834bb951 100644 --- a/homeassistant/components/open_meteo/config_flow.py +++ b/homeassistant/components/open_meteo/config_flow.py @@ -5,10 +5,11 @@ import voluptuous as vol -from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, ENTITY_ID_HOME +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ZONE from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import selector from .const import DOMAIN @@ -22,31 +23,22 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - zones: dict[str, str] = { - entity_id: state.name - for entity_id in self.hass.states.async_entity_ids(ZONE_DOMAIN) - if (state := self.hass.states.get(entity_id)) is not None - } - if user_input is not None: await self.async_set_unique_id(user_input[CONF_ZONE]) self._abort_if_unique_id_configured() + + state = self.hass.states.get(user_input[CONF_ZONE]) return self.async_create_entry( - title=zones[user_input[CONF_ZONE]], + title=state.name if state else "Open-Meteo", data={CONF_ZONE: user_input[CONF_ZONE]}, ) - zones = dict(sorted(zones.items(), key=lambda x: x[1], reverse=True)) - return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_ZONE): vol.In( - { - ENTITY_ID_HOME: zones.pop(ENTITY_ID_HOME), - **zones, - } + vol.Required(CONF_ZONE): selector( + {"entity": {"domain": ZONE_DOMAIN}} ), } ), diff --git a/homeassistant/components/open_meteo/translations/el.json b/homeassistant/components/open_meteo/translations/el.json new file mode 100644 index 0000000000000..dd95b5e28d70f --- /dev/null +++ b/homeassistant/components/open_meteo/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "\u0396\u03ce\u03bd\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/lv.json b/homeassistant/components/open_meteo/translations/lv.json new file mode 100644 index 0000000000000..84524b2078660 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/open_meteo/translations/pt-BR.json b/homeassistant/components/open_meteo/translations/pt-BR.json new file mode 100644 index 0000000000000..44ef054dceac2 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zona" + }, + "description": "Selecione o local a ser usado para previs\u00e3o do tempo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index b1e3b0597b5a2..9a05015496965 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], "requirements": ["openerz-api==0.1.0"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["openerz_api"] } diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index c4e5a5b7711f6..3a8984af25384 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": ["openevsewifi==1.1.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["openevsewifi"] } diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 76ffcc42bd169..eb1b50db5b698 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -66,7 +66,7 @@ def __init__( hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=5), ) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index cd7d0f48eec77..a76c7d16d7473 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -9,5 +9,6 @@ "open-garage==0.2.0" ], "iot_class": "local_polling", - "config_flow": true + "config_flow": true, + "loggers": ["opengarage"] } \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/el.json b/homeassistant/components/opengarage/translations/el.json new file mode 100644 index 0000000000000..d137f362508bb --- /dev/null +++ b/homeassistant/components/opengarage/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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "device_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/pt-BR.json b/homeassistant/components/opengarage/translations/pt-BR.json new file mode 100644 index 0000000000000..dbbd78229d2ca --- /dev/null +++ b/homeassistant/components/opengarage/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", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "device_key": "Chave do dispositivo", + "host": "Nome do host", + "port": "Porta", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/sk.json b/homeassistant/components/opengarage/translations/sk.json new file mode 100644 index 0000000000000..1145b3bb9f844 --- /dev/null +++ b/homeassistant/components/opengarage/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index c83b135cb8ad3..6b8815f931864 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==2.0.1"], "codeowners": ["@bazwilliams"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["async_upnp_client", "openhomedevice"] } diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index df750156d1de9..513cb5ac3da26 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": ["opensensemap-api==0.1.5"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["opensensemap_api"] } diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 1228bc87df1e8..b4278bcce3632 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -26,6 +26,7 @@ CONF_ALTITUDE = "altitude" +ATTR_ICAO24 = "icao24" ATTR_CALLSIGN = "callsign" ATTR_ALTITUDE = "altitude" ATTR_ON_GROUND = "on_ground" @@ -45,7 +46,7 @@ ) OPENSKY_API_URL = "https://opensky-network.org/api/states/all" OPENSKY_API_FIELDS = [ - "icao24", + ATTR_ICAO24, ATTR_CALLSIGN, "origin_country", "time_position", @@ -128,11 +129,13 @@ def _handle_boundary(self, flights, event, metadata): altitude = metadata[flight].get(ATTR_ALTITUDE) longitude = metadata[flight].get(ATTR_LONGITUDE) latitude = metadata[flight].get(ATTR_LATITUDE) + icao24 = metadata[flight].get(ATTR_ICAO24) else: # Assume Flight has landed if missing. altitude = 0 longitude = None latitude = None + icao24 = None data = { ATTR_CALLSIGN: flight, @@ -140,6 +143,7 @@ def _handle_boundary(self, flights, event, metadata): ATTR_SENSOR: self._name, ATTR_LONGITUDE: longitude, ATTR_LATITUDE: latitude, + ATTR_ICAO24: icao24, } self._hass.bus.fire(event, data) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index ba934fefa1251..1ea24ae970949 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -83,7 +83,7 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] -async def options_updated(hass, entry): +async def options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] async_dispatcher_send(hass, gateway.options_update_signal, entry) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 463a0aa105289..7aa19224020e7 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyotgw==1.1b1"], "codeowners": ["@mvn23"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyotgw"] } diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json index f15bc7bdc0e25..92dac2e9b3dfb 100644 --- a/homeassistant/components/opentherm_gw/translations/el.json +++ b/homeassistant/components/opentherm_gw/translations/el.json @@ -1,11 +1,31 @@ { + "config": { + "error": { + "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", + "id_exists": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" + }, + "step": { + "init": { + "data": { + "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "title": "\u03a0\u03cd\u03bb\u03b7 OpenTherm" + } + } + }, "options": { "step": { "init": { "data": { + "floor_temperature": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03b4\u03b1\u03c0\u03ad\u03b4\u03bf\u03c5", "read_precision": "\u0394\u03b9\u03ac\u03b2\u03b1\u03c3\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", - "set_precision": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2" - } + "set_precision": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2", + "temporary_override_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 OpenTherm" } } } diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json new file mode 100644 index 0000000000000..cf4ed0846d2cc --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "id_exists": "ID do gateway j\u00e1 existe" + }, + "step": { + "init": { + "data": { + "device": "Caminho ou URL", + "id": "ID", + "name": "Nome" + }, + "title": "OpenTherm Gateway" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura do piso", + "read_precision": "Precis\u00e3o de leitura", + "set_precision": "Definir precis\u00e3o", + "temporary_override_mode": "Modo de substitui\u00e7\u00e3o tempor\u00e1ria do ponto de ajuste" + }, + "description": "Op\u00e7\u00f5es para o OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/sk.json b/homeassistant/components/opentherm_gw/translations/sk.json new file mode 100644 index 0000000000000..e7a2eaabb7b13 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 6132cda271092..08299ca5ddbe6 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": ["pyopenuv==2021.11.0"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyopenuv"] } diff --git a/homeassistant/components/openuv/translations/cs.json b/homeassistant/components/openuv/translations/cs.json index 0a674b6aff66b..5e7d7b2d7cc04 100644 --- a/homeassistant/components/openuv/translations/cs.json +++ b/homeassistant/components/openuv/translations/cs.json @@ -17,5 +17,16 @@ "title": "Vypl\u0148te va\u0161e \u00fadaje" } } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "Po\u010d\u00e1te\u010dn\u00ed UV index pro ochrann\u00e9 okno", + "to_window": "Kone\u010dn\u00fd UV index pro ochrann\u00e9 okno" + }, + "title": "Nastaven\u00ed OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/el.json b/homeassistant/components/openuv/translations/el.json index 3f2c11281fada..c86a90e1f09cd 100644 --- a/homeassistant/components/openuv/translations/el.json +++ b/homeassistant/components/openuv/translations/el.json @@ -1,7 +1,19 @@ { "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" + }, + "error": { + "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", + "elevation": "\u0391\u03bd\u03cd\u03c8\u03c9\u03c3\u03b7", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + }, "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" } } diff --git a/homeassistant/components/openuv/translations/pt-BR.json b/homeassistant/components/openuv/translations/pt-BR.json index 01a756a2ebbb2..1fe9216bdad69 100644 --- a/homeassistant/components/openuv/translations/pt-BR.json +++ b/homeassistant/components/openuv/translations/pt-BR.json @@ -1,12 +1,15 @@ { "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { "user": { "data": { - "api_key": "Chave de API do OpenUV", + "api_key": "Chave da API", "elevation": "Eleva\u00e7\u00e3o", "latitude": "Latitude", "longitude": "Longitude" @@ -14,5 +17,16 @@ "title": "Preencha suas informa\u00e7\u00f5es" } } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "Iniciar \u00edndice UV para a janela de prote\u00e7\u00e3o", + "to_window": "Fim do \u00edndice UV para a janela de prote\u00e7\u00e3o" + }, + "title": "Configurar OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/sk.json b/homeassistant/components/openuv/translations/sk.json new file mode 100644 index 0000000000000..19eed2a3fe15f --- /dev/null +++ b/homeassistant/components/openuv/translations/sk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "elevation": "Nadmorsk\u00e1 v\u00fd\u0161ka", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + }, + "title": "Vypl\u0148te svoje \u00fadaje" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "Po\u010diato\u010dn\u00fd UV index pre ochrann\u00e9 okno", + "to_window": "Kone\u010dn\u00fd UV index pre ochrann\u00e9 okno" + }, + "title": "Konfigur\u00e1cia OpenUV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 0b0114328acb6..8146dad908c44 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": ["pyowm==3.2.0"], "codeowners": ["@fabaff", "@freekode", "@nzapponi"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geojson", "pyowm", "pysocks"] } diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index 9c703a704e899..1455ed91a5391 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenWeatherMap \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \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": { diff --git a/homeassistant/components/openweathermap/translations/pt-BR.json b/homeassistant/components/openweathermap/translations/pt-BR.json new file mode 100644 index 0000000000000..dd88767bb6139 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "language": "Idioma", + "latitude": "Latitude", + "longitude": "Longitude", + "mode": "Modo", + "name": "Nome da integra\u00e7\u00e3o" + }, + "description": "Configure a integra\u00e7\u00e3o do OpenWeatherMap. Para gerar a chave de API, acesse https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Idioma", + "mode": "Modo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/sk.json b/homeassistant/components/openweathermap/translations/sk.json new file mode 100644 index 0000000000000..f14039f810f8b --- /dev/null +++ b/homeassistant/components/openweathermap/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json index ed390278969cf..7e8b933fc17da 100644 --- a/homeassistant/components/opnsense/manifest.json +++ b/homeassistant/components/opnsense/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opnsense", "requirements": ["pyopnsense==0.2.0"], "codeowners": ["@mtreinish"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pbr", "pyopnsense"] } diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index 1f0360e265a36..61a94ffda30c5 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": ["pyoppleio==1.0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyoppleio"] } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 9b5f567c4200e..b4cda33ee806d 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": ["OPi.GPIO==0.5.2"], "codeowners": ["@pascallj"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["OPi", "nanopi", "orangepi"] } diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index 0d023a96ad5b8..bd755f38e7ebd 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/oru", "codeowners": ["@bvlaicu"], "requirements": ["oru==0.1.11"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["oru"] } diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 94c7391b6492c..74685b5637307 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": ["orvibo==1.1.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["orvibo"] } diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 0596d4073eba5..6143853f188ec 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": ["lightify==1.0.7.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["lightify"] } diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 14205b8652de7..0c16e660aa91e 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyotp==2.6.0"], "codeowners": [], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyotp"] } diff --git a/homeassistant/components/overkiz/climate.py b/homeassistant/components/overkiz/climate.py new file mode 100644 index 0000000000000..a94c731ec8f9f --- /dev/null +++ b/homeassistant/components/overkiz/climate.py @@ -0,0 +1,26 @@ +"""Support for Overkiz climate 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 .climate_entities import WIDGET_TO_CLIMATE_ENTITY +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz climate from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator) + for device in data.platforms[Platform.CLIMATE] + if device.widget in WIDGET_TO_CLIMATE_ENTITY + ) diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py new file mode 100644 index 0000000000000..0e98b7c7e21d7 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -0,0 +1,8 @@ +"""Climate entities for the Overkiz (by Somfy) integration.""" +from pyoverkiz.enums.ui import UIWidget + +from .atlantic_electrical_heater import AtlanticElectricalHeater + +WIDGET_TO_CLIMATE_ENTITY = { + UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, +} diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py new file mode 100644 index 0000000000000..8756b768e5220 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -0,0 +1,73 @@ +"""Support for Atlantic Electrical Heater.""" +from __future__ import annotations + +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, + ClimateEntity, +) +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, +) +from homeassistant.components.overkiz.entity import OverkizEntity +from homeassistant.const import TEMP_CELSIUS + +PRESET_FROST_PROTECTION = "frost_protection" + +OVERKIZ_TO_HVAC_MODES: dict[str, str] = { + OverkizCommandParam.ON: HVAC_MODE_HEAT, + OverkizCommandParam.COMFORT: HVAC_MODE_HEAT, + OverkizCommandParam.OFF: HVAC_MODE_OFF, +} +HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()} + +OVERKIZ_TO_PRESET_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_NONE, + OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.COMFORT: PRESET_COMFORT, +} + +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} + + +class AtlanticElectricalHeater(OverkizEntity, ClimateEntity): + """Representation of Atlantic Electrical Heater.""" + + _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + _attr_supported_features = SUPPORT_PRESET_MODE + _attr_temperature_unit = TEMP_CELSIUS + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODES[ + cast(str, self.executor.select_state(OverkizState.CORE_ON_OFF)) + ] + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_LEVEL, HVAC_MODES_TO_OVERKIZ[hvac_mode] + ) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., home, away, temp.""" + return OVERKIZ_TO_PRESET_MODES[ + cast(str, self.executor.select_state(OverkizState.IO_TARGET_HEATING_LEVEL)) + ] + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_LEVEL, PRESET_MODES_TO_OVERKIZ[preset_mode] + ) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 1a0a94198cc3f..f35941d677326 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import CONF_HUB, DEFAULT_HUB, DOMAIN, LOGGER @@ -46,18 +46,17 @@ async def async_validate_input(self, user_input: dict[str, Any]) -> None: username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] server = SUPPORTED_SERVERS[user_input[CONF_HUB]] - session = async_get_clientsession(self.hass) + session = async_create_clientsession(self.hass) - client = OverkizClient( + async with OverkizClient( username=username, password=password, server=server, session=session - ) - - await client.login() + ) as client: + await client.login() - # Set first gateway id as unique id - if gateways := await client.get_gateways(): - gateway_id = gateways[0].id - await self.async_set_unique_id(gateway_id) + # Set first gateway id as unique id + if gateways := await client.get_gateways(): + gateway_id = gateways[0].id + await self.async_set_unique_id(gateway_id) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -66,6 +65,9 @@ async def async_step_user( errors = {} if user_input: + self._default_user = user_input[CONF_USERNAME] + self._default_hub = user_input[CONF_HUB] + try: await self.async_validate_input(user_input) except TooManyRequestsException: @@ -128,11 +130,7 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes gateway_id = hostname[8:22] LOGGER.debug("DHCP discovery detected gateway %s", obfuscate_id(gateway_id)) - - await self.async_set_unique_id(gateway_id) - self._abort_if_unique_id_configured() - - return await self.async_step_user() + return await self._process_discovery(gateway_id) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo @@ -144,9 +142,13 @@ async def async_step_zeroconf( gateway_id = properties["gateway_pin"] LOGGER.debug("ZeroConf discovery detected gateway %s", obfuscate_id(gateway_id)) + return await self._process_discovery(gateway_id) + async def _process_discovery(self, gateway_id: str) -> FlowResult: + """Handle discovery of a gateway.""" await self.async_set_unique_id(gateway_id) self._abort_if_unique_id_configured() + self.context["title_placeholders"] = {"gateway_id": gateway_id} return await self.async_step_user() diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index ba5470724cd3a..7f031ef3b6a27 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -5,8 +5,7 @@ import logging from typing import Final -from pyoverkiz.enums import UIClass -from pyoverkiz.enums.ui import UIWidget +from pyoverkiz.enums import OverkizCommandParam, UIClass, UIWidget from homeassistant.const import Platform @@ -22,12 +21,14 @@ PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, Platform.SCENE, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, ] @@ -37,7 +38,7 @@ ] # Used to map the Somfy widget and ui_class to the Home Assistant platform -OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform] = { +OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.ADJUSTABLE_SLATS_ROLLER_SHUTTER: Platform.COVER, UIClass.AWNING: Platform.COVER, UIClass.CURTAIN: Platform.COVER, @@ -52,26 +53,31 @@ UIClass.ROLLER_SHUTTER: Platform.COVER, UIClass.SCREEN: Platform.COVER, UIClass.SHUTTER: Platform.COVER, + UIClass.SIREN: Platform.SIREN, UIClass.SWIMMING_POOL: Platform.SWITCH, UIClass.SWINGING_SHUTTER: Platform.COVER, UIClass.VENETIAN_BLIND: Platform.COVER, UIClass.WINDOW: Platform.COVER, + UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) - UIWidget.MY_FOX_SECURITY_CAMERA: Platform.COVER, # widgetName, uiClass is Camera (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) UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported) + UIWidget.SIREN_STATUS: None, # widgetName, uiClass is Siren (siren) + UIWidget.STATELESS_ALARM_CONTROLLER: Platform.SWITCH, # widgetName, uiClass is Alarm (not supported) + UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported) } # Map Overkiz camelCase to Home Assistant snake_case for translation OVERKIZ_STATE_TO_TRANSLATION: dict[str, str] = { - "externalGateway": "external_gateway", - "localUser": "local_user", - "lowBattery": "low_battery", - "LSC": "lsc", - "maintenanceRequired": "maintenance_required", - "noDefect": "no_defect", - "SAAC": "saac", - "SFC": "sfc", - "UPS": "ups", + OverkizCommandParam.EXTERNAL_GATEWAY: "external_gateway", + OverkizCommandParam.LOCAL_USER: "local_user", + OverkizCommandParam.LOW_BATTERY: "low_battery", + OverkizCommandParam.LSC: "lsc", + OverkizCommandParam.MAINTENANCE_REQUIRED: "maintenance_required", + OverkizCommandParam.NO_DEFECT: "no_defect", + OverkizCommandParam.SAAC: "saac", + OverkizCommandParam.SFC: "sfc", + OverkizCommandParam.UPS: "ups", } diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py index ff7cd429ea5fe..d90a52ae40952 100644 --- a/homeassistant/components/overkiz/coordinator.py +++ b/homeassistant/components/overkiz/coordinator.py @@ -1,16 +1,20 @@ """Helpers to help coordinate updates.""" from __future__ import annotations +from collections.abc import Callable, Coroutine from datetime import timedelta import logging +from typing import Any from aiohttp import ServerDisconnectedError from pyoverkiz.client import OverkizClient -from pyoverkiz.enums import EventName, ExecutionState +from pyoverkiz.enums import EventName, ExecutionState, Protocol from pyoverkiz.exceptions import ( BadCredentialsException, + InvalidEventListenerIdException, MaintenanceException, NotAuthenticatedException, + TooManyConcurrentRequestsException, TooManyRequestsException, ) from pyoverkiz.models import Device, Event, Place @@ -23,7 +27,9 @@ from .const import DOMAIN, LOGGER, UPDATE_INTERVAL -EVENT_HANDLERS = Registry() +EVENT_HANDLERS: Registry[ + str, Callable[[OverkizDataUpdateCoordinator, Event], Coroutine[Any, Any, None]] +] = Registry() class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]): @@ -53,9 +59,7 @@ def __init__( self.client = client self.devices: dict[str, Device] = {d.device_url: d for d in devices} self.is_stateless = all( - device.device_url.startswith("rts://") - or device.device_url.startswith("internal://") - for device in devices + device.protocol in (Protocol.RTS, Protocol.INTERNAL) for device in devices ) self.executions: dict[str, dict[str, str]] = {} self.areas = self._places_to_area(places) @@ -67,10 +71,14 @@ async def _async_update_data(self) -> dict[str, Device]: events = await self.client.fetch_events() except BadCredentialsException as exception: raise ConfigEntryAuthFailed("Invalid authentication.") from exception + except TooManyConcurrentRequestsException as exception: + raise UpdateFailed("Too many concurrent requests.") from exception except TooManyRequestsException as exception: raise UpdateFailed("Too many requests, try again later.") from exception except MaintenanceException as exception: raise UpdateFailed("Server is down for maintenance.") from exception + except InvalidEventListenerIdException as exception: + raise UpdateFailed(exception) from exception except TimeoutError as exception: raise UpdateFailed("Failed to connect.") from exception except (ServerDisconnectedError, NotAuthenticatedException): diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index a87b3b5edd67a..4e741aa68e6de 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -1,5 +1,5 @@ """Support for Overkiz covers - shutters etc.""" -from pyoverkiz.enums import UIClass +from pyoverkiz.enums import OverkizCommand, UIClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -10,7 +10,7 @@ from .const import DOMAIN from .cover_entities.awning import Awning from .cover_entities.generic_cover import OverkizGenericCover -from .cover_entities.vertical_cover import VerticalCover +from .cover_entities.vertical_cover import LowSpeedCover, VerticalCover async def async_setup_entry( @@ -31,4 +31,10 @@ async def async_setup_entry( if device.ui_class != UIClass.AWNING ] + entities += [ + LowSpeedCover(device.device_url, data.coordinator) + for device in data.platforms[Platform.COVER] + if OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED in device.definition.commands + ] + async_add_entities(entities) diff --git a/homeassistant/components/overkiz/cover_entities/awning.py b/homeassistant/components/overkiz/cover_entities/awning.py index bbce2c985edd1..ebbff8710f3fc 100644 --- a/homeassistant/components/overkiz/cover_entities/awning.py +++ b/homeassistant/components/overkiz/cover_entities/awning.py @@ -7,11 +7,11 @@ from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_AWNING, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, ) from .generic_cover import COMMANDS_STOP, OverkizGenericCover @@ -20,7 +20,7 @@ class Awning(OverkizGenericCover): """Representation of an Overkiz awning.""" - _attr_device_class = DEVICE_CLASS_AWNING + _attr_device_class = CoverDeviceClass.AWNING @property def supported_features(self) -> int: @@ -56,9 +56,8 @@ def current_cover_position(self) -> int | None: async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - position = kwargs.get(ATTR_POSITION, 0) await self.executor.async_execute_command( - OverkizCommand.SET_DEPLOYMENT, position + OverkizCommand.SET_DEPLOYMENT, kwargs[ATTR_POSITION] ) async def async_open_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/overkiz/cover_entities/generic_cover.py b/homeassistant/components/overkiz/cover_entities/generic_cover.py index 60484620df172..c25cd1ab8067b 100644 --- a/homeassistant/components/overkiz/cover_entities/generic_cover.py +++ b/homeassistant/components/overkiz/cover_entities/generic_cover.py @@ -64,7 +64,7 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: if command := self.executor.select_command(*COMMANDS_SET_TILT_POSITION): await self.executor.async_execute_command( command, - 100 - kwargs.get(ATTR_TILT_POSITION, 0), + 100 - kwargs[ATTR_TILT_POSITION], ) @property diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index 6e69f24f2f166..ec502a403ad94 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -1,24 +1,25 @@ """Support for Overkiz Vertical Covers.""" from __future__ import annotations -from typing import Any, Union, cast - -from pyoverkiz.enums import OverkizCommand, OverkizState, UIClass, UIWidget +from typing import Any, cast + +from pyoverkiz.enums import ( + OverkizCommand, + OverkizCommandParam, + OverkizState, + UIClass, + UIWidget, +) from homeassistant.components.cover import ( ATTR_POSITION, - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_GATE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, SUPPORT_STOP, + CoverDeviceClass, ) +from homeassistant.components.overkiz.coordinator import OverkizDataUpdateCoordinator from .generic_cover import COMMANDS_STOP, OverkizGenericCover @@ -26,16 +27,16 @@ COMMANDS_CLOSE = [OverkizCommand.CLOSE, OverkizCommand.DOWN, OverkizCommand.CYCLE] OVERKIZ_DEVICE_TO_DEVICE_CLASS = { - UIClass.CURTAIN: DEVICE_CLASS_CURTAIN, - UIClass.EXTERIOR_SCREEN: DEVICE_CLASS_BLIND, - UIClass.EXTERIOR_VENETIAN_BLIND: DEVICE_CLASS_BLIND, - UIClass.GARAGE_DOOR: DEVICE_CLASS_GARAGE, - UIClass.GATE: DEVICE_CLASS_GATE, - UIWidget.MY_FOX_SECURITY_CAMERA: DEVICE_CLASS_SHUTTER, - UIClass.PERGOLA: DEVICE_CLASS_AWNING, - UIClass.ROLLER_SHUTTER: DEVICE_CLASS_SHUTTER, - UIClass.SWINGING_SHUTTER: DEVICE_CLASS_SHUTTER, - UIClass.WINDOW: DEVICE_CLASS_WINDOW, + UIClass.CURTAIN: CoverDeviceClass.CURTAIN, + UIClass.EXTERIOR_SCREEN: CoverDeviceClass.BLIND, + UIClass.EXTERIOR_VENETIAN_BLIND: CoverDeviceClass.BLIND, + UIClass.GARAGE_DOOR: CoverDeviceClass.GARAGE, + UIClass.GATE: CoverDeviceClass.GATE, + UIWidget.MY_FOX_SECURITY_CAMERA: CoverDeviceClass.SHUTTER, + UIClass.PERGOLA: CoverDeviceClass.AWNING, + UIClass.ROLLER_SHUTTER: CoverDeviceClass.SHUTTER, + UIClass.SWINGING_SHUTTER: CoverDeviceClass.SHUTTER, + UIClass.WINDOW: CoverDeviceClass.WINDOW, } @@ -69,7 +70,7 @@ def device_class(self) -> str: ( OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.widget) or OVERKIZ_DEVICE_TO_DEVICE_CLASS.get(self.device.ui_class) - or DEVICE_CLASS_BLIND + or CoverDeviceClass.BLIND ), ) @@ -80,24 +81,20 @@ def current_cover_position(self) -> int | None: None is unknown, 0 is closed, 100 is fully open. """ - position = cast( - Union[int, None], - self.executor.select_state( - OverkizState.CORE_CLOSURE, - OverkizState.CORE_CLOSURE_OR_ROCKER_POSITION, - OverkizState.CORE_PEDESTRIAN_POSITION, - ), + position = self.executor.select_state( + OverkizState.CORE_CLOSURE, + OverkizState.CORE_CLOSURE_OR_ROCKER_POSITION, + OverkizState.CORE_PEDESTRIAN_POSITION, ) - # Uno devices can have a position not in 0 to 100 range when unknown - if position is None or position < 0 or position > 100: + if position is None: return None - return 100 - position + return 100 - cast(int, position) async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - position = 100 - kwargs.get(ATTR_POSITION, 0) + position = 100 - kwargs[ATTR_POSITION] await self.executor.async_execute_command(OverkizCommand.SET_CLOSURE, position) async def async_open_cover(self, **kwargs: Any) -> None: @@ -109,3 +106,39 @@ async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if command := self.executor.select_command(*COMMANDS_CLOSE): await self.executor.async_execute_command(command) + + +class LowSpeedCover(VerticalCover): + """Representation of an Overkiz Low Speed cover.""" + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator) + self._attr_name = f"{self._attr_name} Low Speed" + self._attr_unique_id = f"{self._attr_unique_id}_low_speed" + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self.async_set_cover_position_low_speed(**kwargs) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.async_set_cover_position_low_speed(**{ATTR_POSITION: 100}) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.async_set_cover_position_low_speed(**{ATTR_POSITION: 0}) + + async def async_set_cover_position_low_speed(self, **kwargs: Any) -> None: + """Move the cover to a specific position with a low speed.""" + position = 100 - kwargs.get(ATTR_POSITION, 0) + + await self.executor.async_execute_command( + OverkizCommand.SET_CLOSURE_AND_LINEAR_SPEED, + position, + OverkizCommandParam.LOWSPEED, + ) diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index c362969abf20c..2e9b008081548 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -4,13 +4,22 @@ from typing import Any from urllib.parse import urlparse -from pyoverkiz.enums.command import OverkizCommand +from pyoverkiz.enums import OverkizCommand, Protocol from pyoverkiz.models import Command, Device from pyoverkiz.types import StateType as OverkizStateType -from .const import LOGGER from .coordinator import OverkizDataUpdateCoordinator +# Commands that don't support setting +# the delay to another value +COMMANDS_WITHOUT_DELAY = [ + OverkizCommand.IDENTIFY, + OverkizCommand.OFF, + OverkizCommand.ON, + OverkizCommand.ON_WITH_TIMER, + OverkizCommand.TEST, +] + class OverkizExecutor: """Representation of an Overkiz device with execution handler.""" @@ -59,15 +68,19 @@ def select_attribute(self, *attributes: str) -> OverkizStateType: async def async_execute_command(self, command_name: str, *args: Any) -> None: """Execute device command in async context.""" - try: - exec_id = await self.coordinator.client.execute_command( - self.device.device_url, - Command(command_name, list(args)), - "Home Assistant", - ) - except Exception as exception: # pylint: disable=broad-except - LOGGER.error(exception) - return + # Set the execution duration to 0 seconds for RTS devices on supported commands + # Default execution duration is 30 seconds and will block consecutive commands + if ( + self.device.protocol == Protocol.RTS + and command_name not in COMMANDS_WITHOUT_DELAY + ): + args = args + (0,) + + exec_id = await self.coordinator.client.execute_command( + self.device.device_url, + Command(command_name, list(args)), + "Home Assistant", + ) # ExecutionRegisteredEvent doesn't contain the device_url, thus we need to register it here self.coordinator.executions[exec_id] = { diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7fe3d5d2fea1..5e8fe27e21ec5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": [ - "pyoverkiz==1.3.2" + "pyoverkiz==1.3.9" ], "zeroconf": [ { @@ -23,5 +23,12 @@ "@vlebourl", "@tetienne" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": [ + "boto3", + "botocore", + "pyhumps", + "pyoverkiz", + "s3transfer" + ] } \ No newline at end of file diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index d0543eefd5e49..40d04b9bf71a4 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -48,6 +48,34 @@ class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescription max_value=4, entity_category=EntityCategory.CONFIG, ), + # SomfyHeatingTemperatureInterface + OverkizNumberDescription( + key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, + name="Eco Room Temperature", + icon="mdi:thermometer", + command=OverkizCommand.SET_ECO_TEMPERATURE, + min_value=6, + max_value=29, + entity_category=EntityCategory.CONFIG, + ), + OverkizNumberDescription( + key=OverkizState.CORE_COMFORT_ROOM_TEMPERATURE, + name="Comfort Room Temperature", + icon="mdi:home-thermometer-outline", + command=OverkizCommand.SET_COMFORT_TEMPERATURE, + min_value=7, + max_value=30, + entity_category=EntityCategory.CONFIG, + ), + OverkizNumberDescription( + key=OverkizState.CORE_SECURED_POSITION_TEMPERATURE, + name="Freeze Protection Temperature", + icon="mdi:sun-thermometer-outline", + command=OverkizCommand.SET_SECURED_POSITION_TEMPERATURE, + min_value=5, + max_value=15, + entity_category=EntityCategory.CONFIG, + ), ] SUPPORTED_STATES = {description.key: description for description in NUMBER_DESCRIPTIONS} diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index f1e48d83469f7..c097b04d4ebde 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -73,6 +73,17 @@ def _select_option_memorized_simple_volume( entity_category=EntityCategory.CONFIG, device_class=OverkizDeviceClass.MEMORIZED_SIMPLE_VOLUME, ), + # SomfyHeatingTemperatureInterface + OverkizSelectDescription( + key=OverkizState.OVP_HEATING_TEMPERATURE_INTERFACE_OPERATING_MODE, + name="Operating Mode", + icon="mdi:sun-snowflake", + options=[OverkizCommandParam.HEATING, OverkizCommandParam.COOLING], + select_option=lambda option, execute_command: execute_command( + OverkizCommand.SET_OPERATING_MODE, option + ), + entity_category=EntityCategory.CONFIG, + ), ] SUPPORTED_STATES = {description.key: description for description in SELECT_DESCRIPTIONS} diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 1e5eb0881824c..10de6f699dde0 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -359,6 +359,13 @@ class OverkizSensorDescription(SensorEntityDescription): key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, name="Electric Booster Operating Time", ), + # Cover + OverkizSensorDescription( + key=OverkizState.CORE_TARGET_CLOSURE, + name="Target Closure", + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + ), ] SUPPORTED_STATES = {description.key: description for description in SENSOR_DESCRIPTIONS} diff --git a/homeassistant/components/overkiz/siren.py b/homeassistant/components/overkiz/siren.py new file mode 100644 index 0000000000000..8f9d2d6167ffd --- /dev/null +++ b/homeassistant/components/overkiz/siren.py @@ -0,0 +1,71 @@ +"""Support for Overkiz sirens.""" +from typing import Any + +from pyoverkiz.enums import OverkizState +from pyoverkiz.enums.command import OverkizCommand, OverkizCommandParam + +from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren.const import ( + ATTR_DURATION, + SUPPORT_DURATION, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +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 .entity import OverkizEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz sirens from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + OverkizSiren(device.device_url, data.coordinator) + for device in data.platforms[Platform.SIREN] + ) + + +class OverkizSiren(OverkizEntity, SirenEntity): + """Representation an Overkiz Siren.""" + + _attr_supported_features = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | SUPPORT_DURATION + + @property + def is_on(self) -> bool: + """Get whether the siren is in on state.""" + return ( + self.executor.select_state(OverkizState.CORE_ON_OFF) + == OverkizCommandParam.ON + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Send the on command.""" + if kwargs.get(ATTR_DURATION): + duration = kwargs[ATTR_DURATION] + else: + duration = 2 * 60 # 2 minutes + + duration_in_ms = duration * 1000 + + await self.executor.async_execute_command( + # https://www.tahomalink.com/enduser-mobile-web/steer-html5-client/vendor/somfy/io/siren/const.js + OverkizCommand.RING_WITH_SINGLE_SIMPLE_SEQUENCE, + duration_in_ms, # duration + 75, # 90 seconds bip, 30 seconds silence + 2, # repeat 3 times + OverkizCommandParam.MEMORIZED_VOLUME, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Send the off command.""" + await self.executor.async_execute_command(OverkizCommand.OFF) diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 2bef16ec2dde9..87487d53c6692 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "description": "The Overkiz platform is used by various vendors like Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) and Atlantic (Cozytouch). Enter your application credentials and select your hub.", diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index 969a950894304..8fd38816bcd29 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -17,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData @@ -30,13 +31,14 @@ class OverkizSwitchDescriptionMixin: turn_on: Callable[[Callable[..., Awaitable[None]]], Awaitable[None]] turn_off: Callable[[Callable[..., Awaitable[None]]], Awaitable[None]] - is_on: Callable[[Callable[[str], OverkizStateType]], bool] @dataclass class OverkizSwitchDescription(SwitchEntityDescription, OverkizSwitchDescriptionMixin): """Class to describe an Overkiz switch.""" + is_on: Callable[[Callable[[str], OverkizStateType]], bool] | None = None + SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ OverkizSwitchDescription( @@ -75,14 +77,36 @@ class OverkizSwitchDescription(SwitchEntityDescription, OverkizSwitchDescription turn_on=lambda execute_command: execute_command(OverkizCommand.ON), turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), icon="mdi:bell", - is_on=lambda select_state: False, # Remove when is_on in SwitchEntity doesn't require a bool value anymore ), OverkizSwitchDescription( key=UIWidget.RTD_OUTDOOR_SIREN, turn_on=lambda execute_command: execute_command(OverkizCommand.ON), turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), icon="mdi:bell", - is_on=lambda select_state: False, # Remove when is_on in SwitchEntity doesn't require a bool value anymore + ), + OverkizSwitchDescription( + key=UIWidget.STATELESS_ALARM_CONTROLLER, + turn_on=lambda execute_command: execute_command(OverkizCommand.ALARM_ON), + turn_off=lambda execute_command: execute_command(OverkizCommand.ALARM_OFF), + icon="mdi:shield-lock", + ), + OverkizSwitchDescription( + key=UIWidget.STATELESS_EXTERIOR_HEATING, + turn_on=lambda execute_command: execute_command(OverkizCommand.ON), + turn_off=lambda execute_command: execute_command(OverkizCommand.OFF), + icon="mdi:radiator", + ), + OverkizSwitchDescription( + key=UIWidget.MY_FOX_SECURITY_CAMERA, + name="Camera Shutter", + turn_on=lambda execute_command: execute_command(OverkizCommand.OPEN), + turn_off=lambda execute_command: execute_command(OverkizCommand.CLOSE), + icon="mdi:camera-lock", + is_on=lambda select_state: ( + select_state(OverkizState.MYFOX_SHUTTER_STATUS) + == OverkizCommandParam.OPENED + ), + entity_category=EntityCategory.CONFIG, ), ] @@ -121,9 +145,12 @@ class OverkizSwitch(OverkizDescriptiveEntity, SwitchEntity): entity_description: OverkizSwitchDescription @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return True if entity is on.""" - return self.entity_description.is_on(self.executor.select_state) + if self.entity_description.is_on: + return self.entity_description.is_on(self.executor.select_state) + + return None async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" diff --git a/homeassistant/components/overkiz/translations/bg.json b/homeassistant/components/overkiz/translations/bg.json index aaee76bbb8d5b..afee50a6b0067 100644 --- a/homeassistant/components/overkiz/translations/bg.json +++ b/homeassistant/components/overkiz/translations/bg.json @@ -1,7 +1,9 @@ { "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": "\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_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": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -10,6 +12,7 @@ "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" }, + "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index ae269e1186767..1e1bccd1fb714 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_wrong_account": "Nom\u00e9s pots tornar a autenticar aquesta entrada amb el mateix compte i hub d'Overkiz" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -10,6 +12,7 @@ "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", "unknown": "Error inesperat" }, + "flow_title": "Passarel\u00b7la: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/cs.json b/homeassistant/components/overkiz/translations/cs.json index 253dac9764dc1..95baa6367202f 100644 --- a/homeassistant/components/overkiz/translations/cs.json +++ b/homeassistant/components/overkiz/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "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/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index 27719b662fdf1..84d09f9116bcc 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "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" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -10,6 +12,7 @@ "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 3e8b72613ce1d..2ba577de4c5fe 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -1,13 +1,25 @@ { "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", + "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_wrong_account": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03bc\u03cc\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03ba\u03cc\u03bc\u03b2\u03bf Overkiz." + }, "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", "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", - "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." + "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" }, + "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", "step": { "user": { "data": { - "hub": "\u039a\u03cc\u03bc\u03b2\u03bf\u03c2" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "hub": "\u039a\u03cc\u03bc\u03b2\u03bf\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" }, "description": "\u0397 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1 Overkiz \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03bf\u03c5\u03c2 \u03c0\u03c1\u03bf\u03bc\u03b7\u03b8\u03b5\u03c5\u03c4\u03ad\u03c2 \u03cc\u03c0\u03c9\u03c2 \u03b7 Somfy (Connexoon / TaHoma), \u03b7 Hitachi (Hi Kumo), \u03b7 Rexel (Energeasy Connect) \u03ba\u03b1\u03b9 \u03b7 Atlantic (Cozytouch). \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03cc\u03bc\u03b2\u03bf \u03c3\u03b1\u03c2." } diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 4238090458359..c9551aa555cbc 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -12,6 +12,7 @@ "too_many_requests": "Too many requests, try again later", "unknown": "Unexpected error" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 7b03a8287182c..f1702a6d70321 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -10,6 +10,7 @@ "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", "unknown": "Error inesperado" }, + "flow_title": "Puerta de enlace: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index 53e41c8d7be98..c19d9c39ca985 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_wrong_account": "Seda kirjet saab uuesti autentida ainult sama Overkiz'i konto ja keskuse abil." }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -10,6 +12,7 @@ "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", "unknown": "Ootamatu t\u00f5rge" }, + "flow_title": "L\u00fc\u00fcs: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 83243718739ec..790aafc07966c 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "reauth_wrong_account": "Vous ne pouvez r\u00e9authentifier cette entr\u00e9e qu'avec le m\u00eame compte et hub Overkiz" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -10,6 +12,7 @@ "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue" }, + "flow_title": "Passerelle : {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/he.json b/homeassistant/components/overkiz/translations/he.json index bb0bbef8760bc..a030b22455e00 100644 --- a/homeassistant/components/overkiz/translations/he.json +++ b/homeassistant/components/overkiz/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "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": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index 2129b81aa1050..ef7cacaaf96ef 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_wrong_account": "Ezt csak ugyanazzal az Overkiz fi\u00f3kkal \u00e9s hubbal hiteles\u00edtheti \u00fajra." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -10,6 +12,7 @@ "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" }, + "flow_title": "\u00c1tj\u00e1r\u00f3: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index f1abd93422154..becbae65f9eb4 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_wrong_account": "You can only reauthenticate this entry with the same Overkiz account and hub" }, "error": { "cannot_connect": "Gagal terhubung", @@ -10,6 +12,7 @@ "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json index 2f9c288aaac4b..00898513f1fd6 100644 --- a/homeassistant/components/overkiz/translations/it.json +++ b/homeassistant/components/overkiz/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_wrong_account": "Puoi riautenticare questa voce solo con lo stesso account e hub Overkiz" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -10,6 +12,7 @@ "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.", "unknown": "Errore imprevisto" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index 6ff74c5a61eed..e978a8f36f2a7 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "reauth_wrong_account": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u306f\u3001\u540c\u3058Overkiz account\u3068hub\u3067\u306e\u307f\u518d\u8a8d\u8a3c\u3067\u304d\u307e\u3059" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -10,6 +12,7 @@ "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\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "flow_title": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/nb.json b/homeassistant/components/overkiz/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/overkiz/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index 7c64d862e2f88..d33dd5bb44cad 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol", + "reauth_wrong_account": "U kunt deze invoer alleen opnieuw verifi\u00ebren met hetzelfde Overkiz account en hub" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -10,6 +12,7 @@ "too_many_requests": "Te veel verzoeken, probeer het later opnieuw.", "unknown": "Onverwachte fout" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index fe0b10996c93a..ed691aa388f37 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "reauth_wrong_account": "Du kan bare autentisere denne oppf\u00f8ringen p\u00e5 nytt med samme Overkiz-konto og hub" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -10,6 +12,7 @@ "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", "unknown": "Uventet feil" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 7705581178b5b..6881d7edb5b4e 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_wrong_account": "Mo\u017cesz ponownie uwierzytelni\u0107 ten wpis tylko przy u\u017cyciu tego samego konta Overkiz i huba." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -10,6 +12,7 @@ "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "Bramka: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json new file mode 100644 index 0000000000000..2f2c335ebd534 --- /dev/null +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_wrong_account": "Voc\u00ea s\u00f3 pode reautenticar esta entrada com a mesma conta e hub do Overkiz" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "server_in_maintenance": "O servidor est\u00e1 fora de servi\u00e7o para manuten\u00e7\u00e3o", + "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", + "unknown": "Erro inesperado" + }, + "flow_title": "Gateway: {gateway_id}", + "step": { + "user": { + "data": { + "host": "Nome do host", + "hub": "Hub", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "A plataforma Overkiz \u00e9 utilizada por v\u00e1rios fornecedores como Somfy (Connexoon/TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) e Atlantic (Cozytouch). Insira suas credenciais de aplicativo e selecione seu hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 3373a23e40730..611c478490273 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -1,7 +1,9 @@ { "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": "\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.", + "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.", + "reauth_wrong_account": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u0442\u043e\u0439 \u0436\u0435 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e Overkiz \u0438 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -10,6 +12,7 @@ "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." }, + "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/select.id.json b/homeassistant/components/overkiz/translations/select.id.json new file mode 100644 index 0000000000000..4a8760b91c53e --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.id.json @@ -0,0 +1,13 @@ +{ + "state": { + "overkiz__memorized_simple_volume": { + "highest": "Tertinggi", + "standard": "Standar" + }, + "overkiz__open_closed_pedestrian": { + "closed": "Tertutup", + "open": "Buka", + "pedestrian": "Pejalan Kaki" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/select.pl.json b/homeassistant/components/overkiz/translations/select.pl.json index 00769a3799ec3..0989aec66fee9 100644 --- a/homeassistant/components/overkiz/translations/select.pl.json +++ b/homeassistant/components/overkiz/translations/select.pl.json @@ -5,8 +5,8 @@ "standard": "Normalnie" }, "overkiz__open_closed_pedestrian": { - "closed": "Zamkni\u0119te", - "open": "Otw\u00f3rz", + "closed": "Zamkni\u0119ta", + "open": "Otwarte", "pedestrian": "Pieszy" } } diff --git a/homeassistant/components/overkiz/translations/select.pt-BR.json b/homeassistant/components/overkiz/translations/select.pt-BR.json new file mode 100644 index 0000000000000..623378023961b --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.pt-BR.json @@ -0,0 +1,13 @@ +{ + "state": { + "overkiz__memorized_simple_volume": { + "highest": "Alt\u00edssimo", + "standard": "Padr\u00e3o" + }, + "overkiz__open_closed_pedestrian": { + "closed": "Fechado", + "open": "Aberto", + "pedestrian": "Pedestre" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/select.ru.json b/homeassistant/components/overkiz/translations/select.ru.json new file mode 100644 index 0000000000000..6c4c93ca75323 --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.ru.json @@ -0,0 +1,13 @@ +{ + "state": { + "overkiz__memorized_simple_volume": { + "highest": "\u0421\u0430\u043c\u044b\u0439 \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "standard": "\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439" + }, + "overkiz__open_closed_pedestrian": { + "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u044b\u0439", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044b\u0439", + "pedestrian": "\u041f\u0435\u0448\u0435\u0445\u043e\u0434\u043d\u044b\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.bg.json b/homeassistant/components/overkiz/translations/sensor.bg.json index 40e3aad4dc4ef..b62b4383fc670 100644 --- a/homeassistant/components/overkiz/translations/sensor.bg.json +++ b/homeassistant/components/overkiz/translations/sensor.bg.json @@ -1,11 +1,17 @@ { "state": { "overkiz__priority_lock_originator": { + "local_user": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", "lsc": "LSC", "saac": "SAAC", "sfc": "SFC", "temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430", - "ups": "UPS" + "ups": "UPS", + "user": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b" + }, + "overkiz__sensor_defect": { + "low_battery": "\u0418\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", + "maintenance_required": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.es.json b/homeassistant/components/overkiz/translations/sensor.es.json new file mode 100644 index 0000000000000..8d0c475586b8b --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.es.json @@ -0,0 +1,41 @@ +{ + "state": { + "overkiz__battery": { + "full": "Completo", + "low": "Bajo", + "normal": "Normal", + "verylow": "Muy bajo" + }, + "overkiz__discrete_rssi_level": { + "good": "Bien", + "low": "Bajo", + "normal": "Normal", + "verylow": "Muy bajo" + }, + "overkiz__priority_lock_originator": { + "external_gateway": "Puerta de enlace externa", + "local_user": "Usuario local", + "lsc": "LSC", + "myself": "Yo mismo", + "rain": "Lluvia", + "saac": "SAAC", + "security": "Seguridad", + "sfc": "SFC", + "temperature": "Temperatura", + "timer": "Temporizador", + "ups": "SAI", + "user": "Usuario", + "wind": "Viento" + }, + "overkiz__sensor_defect": { + "dead": "Muerto", + "low_battery": "Bater\u00eda baja", + "maintenance_required": "Mantenimiento necesario", + "no_defect": "Ning\u00fan defecto" + }, + "overkiz__sensor_room": { + "clean": "Limpiar", + "dirty": "Sucio" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.fr.json b/homeassistant/components/overkiz/translations/sensor.fr.json index 07b79c83c3688..af9fd658ab9d4 100644 --- a/homeassistant/components/overkiz/translations/sensor.fr.json +++ b/homeassistant/components/overkiz/translations/sensor.fr.json @@ -9,9 +9,11 @@ "overkiz__discrete_rssi_level": { "good": "Bon", "low": "Bas", - "normal": "Normal" + "normal": "Normal", + "verylow": "Tr\u00e8s lent" }, "overkiz__priority_lock_originator": { + "external_gateway": "Passerelle externe", "local_user": "Utilisateur local", "lsc": "LSC", "myself": "Moi-m\u00eame", diff --git a/homeassistant/components/overkiz/translations/sensor.id.json b/homeassistant/components/overkiz/translations/sensor.id.json new file mode 100644 index 0000000000000..efe4f588a71ea --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.id.json @@ -0,0 +1,41 @@ +{ + "state": { + "overkiz__battery": { + "full": "Penuh", + "low": "Rendah", + "normal": "Normal", + "verylow": "Sangat rendah" + }, + "overkiz__discrete_rssi_level": { + "good": "Bagus", + "low": "Rendah", + "normal": "Normal", + "verylow": "Sangat rendah" + }, + "overkiz__priority_lock_originator": { + "external_gateway": "Gateway eksternal", + "local_user": "Pengguna lokal", + "lsc": "LSC", + "myself": "Saya sendiri", + "rain": "Hujan", + "saac": "SAAC", + "security": "Keamanan", + "sfc": "SFC", + "temperature": "Suhu", + "timer": "Timer", + "ups": "UPS", + "user": "Pengguna", + "wind": "Angin" + }, + "overkiz__sensor_defect": { + "dead": "Mati", + "low_battery": "Baterai lemah", + "maintenance_required": "Diperlukan perawatan", + "no_defect": "Tidak ada cacat" + }, + "overkiz__sensor_room": { + "clean": "Bersih", + "dirty": "Kotor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.lv.json b/homeassistant/components/overkiz/translations/sensor.lv.json new file mode 100644 index 0000000000000..6dda6c1414544 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.lv.json @@ -0,0 +1,20 @@ +{ + "state": { + "overkiz__priority_lock_originator": { + "external_gateway": "\u0100r\u0113j\u0101 v\u0101rteja", + "local_user": "Viet\u0113jais lietot\u0101js", + "lsc": "LSC", + "myself": "Es pats", + "rain": "Lietus", + "saac": "SAAC", + "security": "Dro\u0161\u012bba", + "sfc": "SFC", + "temperature": "Temperat\u016bra", + "ups": "UPS" + }, + "overkiz__sensor_room": { + "clean": "T\u012brs", + "dirty": "Net\u012brs" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.nl.json b/homeassistant/components/overkiz/translations/sensor.nl.json index 628ea98dc7b7a..aef0b1e0394f5 100644 --- a/homeassistant/components/overkiz/translations/sensor.nl.json +++ b/homeassistant/components/overkiz/translations/sensor.nl.json @@ -9,9 +9,11 @@ "overkiz__discrete_rssi_level": { "good": "Goed", "low": "Laag", - "normal": "Normaal" + "normal": "Normaal", + "verylow": "Zeer laag" }, "overkiz__priority_lock_originator": { + "external_gateway": "Externe gateway", "local_user": "Lokale gebruiker", "lsc": "LSC", "myself": "Ikzelf", @@ -26,7 +28,10 @@ "wind": "Wind" }, "overkiz__sensor_defect": { - "dead": "Onbereikbaar" + "dead": "Onbereikbaar", + "low_battery": "Batterij bijna leeg", + "maintenance_required": "Onderhoud vereist", + "no_defect": "Geen defect" }, "overkiz__sensor_room": { "clean": "Schoon", diff --git a/homeassistant/components/overkiz/translations/sensor.pl.json b/homeassistant/components/overkiz/translations/sensor.pl.json index 6b942c21a9a32..0633c0d842484 100644 --- a/homeassistant/components/overkiz/translations/sensor.pl.json +++ b/homeassistant/components/overkiz/translations/sensor.pl.json @@ -1,29 +1,41 @@ { "state": { + "overkiz__battery": { + "full": "pe\u0142na", + "low": "niski", + "normal": "normalny", + "verylow": "bardzo niski" + }, "overkiz__discrete_rssi_level": { - "verylow": "Bardzo niskie" + "good": "dobry", + "low": "s\u0142aby", + "normal": "normalny", + "verylow": "bardzo s\u0142aby" }, "overkiz__priority_lock_originator": { - "external_gateway": "Bramka zewn\u0119trzna", - "local_user": "U\u017cytkownik lokalny", + "external_gateway": "bramka zewn\u0119trzna", + "local_user": "u\u017cytkownik lokalny", + "lsc": "LSC", "myself": "Ja", - "rain": "Deszcz", - "security": "Bezpiecze\u0144stwo", - "temperature": "Temperatura", - "timer": "Minutnik", + "rain": "deszcz", + "saac": "SAAC", + "security": "bezpiecze\u0144stwo", + "sfc": "SFC", + "temperature": "temperatura", + "timer": "minutnik", "ups": "UPS", - "user": "U\u017cytkownik", - "wind": "Wiatr" + "user": "u\u017cytkownik", + "wind": "wiatr" }, "overkiz__sensor_defect": { - "dead": "Martwe", - "low_battery": "Niski poziom baterii", - "maintenance_required": "Wymagany przegl\u0105d", - "no_defect": "Brak uszkodze\u0144" + "dead": "martwe", + "low_battery": "niski poziom baterii", + "maintenance_required": "wymagany przegl\u0105d", + "no_defect": "brak uszkodze\u0144" }, "overkiz__sensor_room": { - "clean": "Czyste", - "dirty": "Brudne" + "clean": "czysto", + "dirty": "brudno" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.pt-BR.json b/homeassistant/components/overkiz/translations/sensor.pt-BR.json index 902bb9167b2dd..3ec82aa83d554 100644 --- a/homeassistant/components/overkiz/translations/sensor.pt-BR.json +++ b/homeassistant/components/overkiz/translations/sensor.pt-BR.json @@ -1,6 +1,29 @@ { "state": { + "overkiz__battery": { + "full": "Completa", + "low": "Baixo", + "normal": "Normal", + "verylow": "Muito baixo" + }, + "overkiz__discrete_rssi_level": { + "good": "Bom", + "low": "Baixo", + "normal": "Normal", + "verylow": "Muito baixo" + }, "overkiz__priority_lock_originator": { + "external_gateway": "Gateway externo", + "local_user": "Usu\u00e1rio local", + "lsc": "LSC", + "myself": "Eu mesmo", + "rain": "Chuva", + "saac": "SAAC", + "security": "Seguran\u00e7a", + "sfc": "SFC", + "temperature": "Temperatura", + "timer": "Temporizador", + "ups": "UPS", "user": "Usu\u00e1rio", "wind": "Vento" }, diff --git a/homeassistant/components/overkiz/translations/sk.json b/homeassistant/components/overkiz/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json new file mode 100644 index 0000000000000..5ba4512fb3595 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", + "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index 42fe10e6a5152..8e6a232db7f29 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "reauth_wrong_account": "Bu giri\u015fi yaln\u0131zca ayn\u0131 Overkiz hesab\u0131 ve hub ile yeniden do\u011frulayabilirsiniz." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -10,6 +12,7 @@ "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin", "unknown": "Beklenmeyen hata" }, + "flow_title": "A\u011f ge\u00e7idi: {gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index f20636f9d1882..04c2fc2b07a09 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_wrong_account": "\u50c5\u80fd\u4f7f\u7528\u76f8\u540c\u7684 Overkiz. \u5e33\u865f\u8207\u96c6\u7dda\u5668\u91cd\u65b0\u8a8d\u8b49\u6b64\u5be6\u9ad4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -10,6 +12,7 @@ "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "\u9598\u9053\u5668\uff1a{gateway_id}", "step": { "user": { "data": { diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index ba559ffb41d4d..6d994e472c856 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -3,7 +3,8 @@ "name": "OVO Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", - "requirements": ["ovoenergy==1.1.12"], + "requirements": ["ovoenergy==1.2.0"], "codeowners": ["@timmo001"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["ovoenergy"] } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 8f9a18d1f113a..532bb25cbc812 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -54,7 +54,9 @@ class OVOEnergySensorEntityDescription(SensorEntityDescription): name="OVO Last Electricity Cost", device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL_INCREASING, - value=lambda usage: usage.electricity[-1].cost.amount, + value=lambda usage: usage.electricity[-1].cost.amount + if usage.electricity[-1].cost is not None + else None, ), OVOEnergySensorEntityDescription( key="last_electricity_start_time", @@ -88,7 +90,9 @@ class OVOEnergySensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:cash-multiple", - value=lambda usage: usage.gas[-1].cost.amount, + value=lambda usage: usage.gas[-1].cost.amount + if usage.gas[-1].cost is not None + else None, ), OVOEnergySensorEntityDescription( key="last_gas_start_time", diff --git a/homeassistant/components/ovo_energy/translations/el.json b/homeassistant/components/ovo_energy/translations/el.json index daedfe647f902..b8b9d35a23830 100644 --- a/homeassistant/components/ovo_energy/translations/el.json +++ b/homeassistant/components/ovo_energy/translations/el.json @@ -1,7 +1,24 @@ { "config": { + "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" + }, + "flow_title": "{username}", "step": { + "reauth": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03bf OVO Energy. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c4\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \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" + }, "user": { + "data": { + "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": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 OVO Energy \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2.", "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd OVO Energy" } diff --git a/homeassistant/components/ovo_energy/translations/pt-BR.json b/homeassistant/components/ovo_energy/translations/pt-BR.json new file mode 100644 index 0000000000000..4b73ecca8b943 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{username}", + "step": { + "reauth": { + "data": { + "password": "Senha" + }, + "description": "Falha na autentica\u00e7\u00e3o para OVO Energy. Por favor, insira suas credenciais atuais.", + "title": "Reautentica\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Configure uma inst\u00e2ncia OVO Energy para acessar seu uso de energia.", + "title": "Adicionar conta OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/sk.json b/homeassistant/components/ovo_energy/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 40dbb7d569c59..1b502481764bc 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["nacl"] } diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index bd01284329b9e..b85a37dadf9c7 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) -HANDLERS = decorator.Registry() +HANDLERS = decorator.Registry() # type: ignore[var-annotated] def get_cipher(): diff --git a/homeassistant/components/owntracks/translations/bg.json b/homeassistant/components/owntracks/translations/bg.json index b69f7ada2a2f5..10bfdae7ae74f 100644 --- a/homeassistant/components/owntracks/translations/bg.json +++ b/homeassistant/components/owntracks/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/owntracks/translations/ca.json b/homeassistant/components/owntracks/translations/ca.json index 236614d06198b..80013ccb55458 100644 --- a/homeassistant/components/owntracks/translations/ca.json +++ b/homeassistant/components/owntracks/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 737b92c642a48..46ca14c81acb6 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/el.json b/homeassistant/components/owntracks/translations/el.json index aecb2ee553fa4..6db4a0fbcba07 100644 --- a/homeassistant/components/owntracks/translations/el.json +++ b/homeassistant/components/owntracks/translations/el.json @@ -1,7 +1,17 @@ { "config": { "abort": { + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "create_entry": { + "default": "\n\n \u03a3\u03c4\u03bf Android, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae OwnTracks] ( {android_url} ), \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9\u03c2 - > \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7. \u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2:\n - \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1: \u0399\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc HTTP\n - \u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {webhook_url}\n - \u03a4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7:\n - \u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: `' '`\n - \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2: `` '` \n\n \u03a3\u03c4\u03bf iOS, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae OwnTracks] ( {ios_url} ), \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf (i) \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac - > \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2. \u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2:\n - \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1: HTTP\n - URL: {webhook_url}\n - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2\n - ID \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: `' '` \n\n {secret}\n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_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." + }, + "step": { + "user": { + "description": "\u0395\u03af\u03c3\u03c4\u03b5 \u03b2\u03ad\u03b2\u03b1\u03b9\u03bf\u03b9 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf OwnTracks;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 OwnTracks" + } } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/en.json b/homeassistant/components/owntracks/translations/en.json index 870b7cdbe5ce4..bf75bfe819004 100644 --- a/homeassistant/components/owntracks/translations/en.json +++ b/homeassistant/components/owntracks/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/et.json b/homeassistant/components/owntracks/translations/et.json index 2ee171365a4ce..1d8a85aba4a21 100644 --- a/homeassistant/components/owntracks/translations/et.json +++ b/homeassistant/components/owntracks/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 35530bd2c86b3..cecdab8643614 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json index d0c3523da94e2..10bd4cb9a4126 100644 --- a/homeassistant/components/owntracks/translations/he.json +++ b/homeassistant/components/owntracks/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json index e99b11a9e7e64..a403bb921c396 100644 --- a/homeassistant/components/owntracks/translations/hu.json +++ b/homeassistant/components/owntracks/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/id.json b/homeassistant/components/owntracks/translations/id.json index 890afaa099cb4..214ea9265cabf 100644 --- a/homeassistant/components/owntracks/translations/id.json +++ b/homeassistant/components/owntracks/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/it.json b/homeassistant/components/owntracks/translations/it.json index 50d6c30777c37..6448d4d957671 100644 --- a/homeassistant/components/owntracks/translations/it.json +++ b/homeassistant/components/owntracks/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index faec1e6977b8b..998478a9cc8b5 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/nb.json b/homeassistant/components/owntracks/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/owntracks/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/nl.json b/homeassistant/components/owntracks/translations/nl.json index 65189e6b0be92..74baf0fe1067d 100644 --- a/homeassistant/components/owntracks/translations/nl.json +++ b/homeassistant/components/owntracks/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json index db992a5630582..7774753a16ee2 100644 --- a/homeassistant/components/owntracks/translations/no.json +++ b/homeassistant/components/owntracks/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/pl.json b/homeassistant/components/owntracks/translations/pl.json index 98c8779fe1f9a..a54a366849d70 100644 --- a/homeassistant/components/owntracks/translations/pl.json +++ b/homeassistant/components/owntracks/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/pt-BR.json b/homeassistant/components/owntracks/translations/pt-BR.json index af1c939be3650..79c42db20e38d 100644 --- a/homeassistant/components/owntracks/translations/pt-BR.json +++ b/homeassistant/components/owntracks/translations/pt-BR.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "create_entry": { - "default": "\n\n No Android, abra [o aplicativo OwnTracks] ( {android_url} ), v\u00e1 para prefer\u00eancias - > conex\u00e3o. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP privado \n - Anfitri\u00e3o: {webhook_url} \n - Identifica\u00e7\u00e3o: \n - Nome de usu\u00e1rio: ` \n - ID do dispositivo: ` ` \n\n No iOS, abra o aplicativo OwnTracks ( {ios_url} ), toque no \u00edcone (i) no canto superior esquerdo - > configura\u00e7\u00f5es. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP \n - URL: {webhook_url} \n - Ativar a autentica\u00e7\u00e3o \n - UserID: ` ` \n\n {secret} \n \n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais informa\u00e7\u00f5es." + "default": "\n\nNo Android, abra [o aplicativo OwnTracks]({android_url}), v\u00e1 para prefer\u00eancias -> conex\u00e3o. Altere as seguintes configura\u00e7\u00f5es:\n - Modo: HTTP privado\n - Anfitri\u00e3o: {webhook_url}\n - Identifica\u00e7\u00e3o:\n - Nome de usu\u00e1rio: `''`\n - ID do dispositivo: `''` \n\nNo iOS, abra o aplicativo OwnTracks ({ios_url}), toque no \u00edcone (i) no canto superior esquerdo -> configura\u00e7\u00f5es. Altere as seguintes configura\u00e7\u00f5es:\n - Modo: HTTP\n - URL: {webhook_url}\n - Ativar a autentica\u00e7\u00e3o\n - UserID: `''`\n\n{secret}\n\nVeja [a documenta\u00e7\u00e3o]({docs_url}) para mais informa\u00e7\u00f5es." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index 09fdba7726683..83e374cdf6cf0 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/tr.json b/homeassistant/components/owntracks/translations/tr.json index 944cd17658015..9f19fa47b7b12 100644 --- a/homeassistant/components/owntracks/translations/tr.json +++ b/homeassistant/components/owntracks/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "create_entry": { diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index e6a6fc26068e9..04cac3dd6eca9 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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." }, "create_entry": { "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json index 2803182629a7b..a9f6016c6c115 100644 --- a/homeassistant/components/owntracks/translations/zh-Hant.json +++ b/homeassistant/components/owntracks/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a `''`\n - Device ID\uff1a`''`\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: `''`\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index bf54f217f24ce..997fbbc5a70b5 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -6,5 +6,6 @@ "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], "after_dependencies": ["mqtt"], "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["openzwavemqtt"] } diff --git a/homeassistant/components/ozw/translations/el.json b/homeassistant/components/ozw/translations/el.json index d365534110baa..6708cec13583e 100644 --- a/homeassistant/components/ozw/translations/el.json +++ b/homeassistant/components/ozw/translations/el.json @@ -1,16 +1,38 @@ { "config": { "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 OpenZWave.", + "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 OpenZWave.", + "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 OpenZWave.", + "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", "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", "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, + "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 OpenZWave. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + }, + "progress": { + "install_addon": "\u03a0\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 OpenZWave. \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." + }, "step": { + "hassio_confirm": { + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenZWave \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave" + }, "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 OpenZWave \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" }, + "on_supervisor": { + "data": { + "use_addon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor" + }, + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor;", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "start_addon": { "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" }, "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave" } diff --git a/homeassistant/components/ozw/translations/pt-BR.json b/homeassistant/components/ozw/translations/pt-BR.json new file mode 100644 index 0000000000000..8ec256d1d75f5 --- /dev/null +++ b/homeassistant/components/ozw/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on OpenZWave.", + "addon_install_failed": "Falha ao instalar o add-on OpenZWave.", + "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o do OpenZWave.", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "addon_start_failed": "Falha ao iniciar o add-on OpenZWave. Verifique a configura\u00e7\u00e3o." + }, + "progress": { + "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do add-on OpenZWave termina. Isso pode levar v\u00e1rios minutos." + }, + "step": { + "hassio_confirm": { + "title": "Configure a integra\u00e7\u00e3o do OpenZWave com o add-on OpenZWave" + }, + "install_addon": { + "title": "A instala\u00e7\u00e3o do add-on OpenZWave foi iniciada" + }, + "on_supervisor": { + "data": { + "use_addon": "Use o add-on OpenZWave Supervisor" + }, + "description": "Deseja usar o add-on OpenZWave Supervisor?", + "title": "Selecione o m\u00e9todo de conex\u00e3o" + }, + "start_addon": { + "data": { + "network_key": "Chave de rede", + "usb_path": "Caminho do Dispositivo USB" + }, + "title": "Digite a configura\u00e7\u00e3o do add-on OpenZWave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sk.json b/homeassistant/components/ozw/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/ozw/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json index f8fb161aa1c4f..f662bc978aedd 100644 --- a/homeassistant/components/ozw/translations/uk.json +++ b/homeassistant/components/ozw/translations/uk.json @@ -7,7 +7,7 @@ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 5ad1ca7ff6b94..0e51da481d730 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -7,7 +7,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "addon_start_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py index 627d0df767ded..b99cc7b86e1bd 100644 --- a/homeassistant/components/p1_monitor/diagnostics.py +++ b/homeassistant/components/p1_monitor/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for P1 Monitor.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -28,8 +29,8 @@ async def async_get_config_entry_diagnostics( "data": async_redact_data(entry.data, TO_REDACT), }, "data": { - "smartmeter": coordinator.data[SERVICE_SMARTMETER].__dict__, - "phases": coordinator.data[SERVICE_PHASES].__dict__, - "settings": coordinator.data[SERVICE_SETTINGS].__dict__, + "smartmeter": asdict(coordinator.data[SERVICE_SMARTMETER]), + "phases": asdict(coordinator.data[SERVICE_PHASES]), + "settings": asdict(coordinator.data[SERVICE_SETTINGS]), }, } diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 1f952d04fc99f..c94893f61fdf4 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -6,5 +6,6 @@ "requirements": ["p1monitor==1.0.1"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["p1monitor"] } diff --git a/homeassistant/components/p1_monitor/translations/el.json b/homeassistant/components/p1_monitor/translations/el.json index 00e89f9735d82..82c79d53acc16 100644 --- a/homeassistant/components/p1_monitor/translations/el.json +++ b/homeassistant/components/p1_monitor/translations/el.json @@ -6,6 +6,10 @@ }, "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" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf {intergration} \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/pt-BR.json b/homeassistant/components/p1_monitor/translations/pt-BR.json new file mode 100644 index 0000000000000..777a1fc5633cb --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Configure o P1 Monitor para integrar com o Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/sk.json b/homeassistant/components/p1_monitor/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index a9d6a4ebf76cb..19ea941cb52e1 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": ["panacotta==0.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["panacotta"] } diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index fe365f85f2cea..5b334f57c98fb 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -5,5 +5,6 @@ "requirements": ["panasonic_viera==0.3.6"], "codeowners": [], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["panasonic_viera"] } diff --git a/homeassistant/components/panasonic_viera/translations/el.json b/homeassistant/components/panasonic_viera/translations/el.json index de3abd8dca713..a55c760ec812f 100644 --- a/homeassistant/components/panasonic_viera/translations/el.json +++ b/homeassistant/components/panasonic_viera/translations/el.json @@ -1,6 +1,12 @@ { "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", + "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", "invalid_pin_code": "\u039f \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03bf\u03c5 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b1\u03c4\u03b5 \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf" }, "step": { @@ -12,6 +18,10 @@ "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" }, "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, "description": "\u03a0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 Panasonic Viera TV \u03c3\u03b1\u03c2 \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2" } diff --git a/homeassistant/components/panasonic_viera/translations/pt-BR.json b/homeassistant/components/panasonic_viera/translations/pt-BR.json index ae60c2dcdbaa0..8cc89f6215bb5 100644 --- a/homeassistant/components/panasonic_viera/translations/pt-BR.json +++ b/homeassistant/components/panasonic_viera/translations/pt-BR.json @@ -1,11 +1,28 @@ { "config": { "abort": { - "already_configured": "Esta TV Panasonic Viera j\u00e1 est\u00e1 configurada." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_pin_code": "C\u00f3digo PIN" }, "step": { + "pairing": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "C\u00f3digo PIN", + "title": "Pareamento" + }, "user": { - "description": "Digite o endere\u00e7o IP da sua TV Panasonic Viera", + "data": { + "host": "Endere\u00e7o IP", + "name": "Nome" + }, + "description": "Digite o Endere\u00e7o IP da sua TV Panasonic Viera", "title": "Configure sua TV" } } diff --git a/homeassistant/components/panasonic_viera/translations/sk.json b/homeassistant/components/panasonic_viera/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 45f87b36ec16f..6cbf8a76f4a47 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": ["pexpect==4.6.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pexpect", "ptyprocess"] } diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 45e8f55a79042..7866b99221e6a 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -253,7 +253,7 @@ def _query_for_playing_status(self): try: match_idx = self._pianobar.expect( [ - br"(\d\d):(\d\d)/(\d\d):(\d\d)", + rb"(\d\d):(\d\d)/(\d\d):(\d\d)", "No song playing", "Select station", "Receiving new playlist", diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index 2e685a8625c51..fc8214265422c 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pcal9535a", "requirements": ["pcal9535a==0.7"], "codeowners": ["@Shulyaka"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pcal9535a", "smbus_cffi"] } diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index e8b44173fe91d..a80cfb12876b3 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": ["pencompy==0.0.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pencompy"] } diff --git a/homeassistant/components/person/translations/uk.json b/homeassistant/components/person/translations/uk.json index 5e6b186e38ccf..ca42365d84b40 100644 --- a/homeassistant/components/person/translations/uk.json +++ b/homeassistant/components/person/translations/uk.json @@ -5,5 +5,5 @@ "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430" } }, - "title": "\u041b\u044e\u0434\u0438\u043d\u0430" + "title": "\u041e\u0441\u043e\u0431\u0430" } \ No newline at end of file diff --git a/homeassistant/components/philips_js/diagnostics.py b/homeassistant/components/philips_js/diagnostics.py new file mode 100644 index 0000000000000..889b8e47e3f2e --- /dev/null +++ b/homeassistant/components/philips_js/diagnostics.py @@ -0,0 +1,59 @@ +"""Diagnostics support for Philips JS.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import PhilipsTVDataUpdateCoordinator +from .const import DOMAIN + +TO_REDACT = { + "serialnumber_encrypted", + "serialnumber", + "deviceid_encrypted", + "deviceid", + "username", + "password", + "title", + "unique_id", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + api = coordinator.api + + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "data": { + "system": async_redact_data(api.system, TO_REDACT), + "powerstate": api.powerstate, + "context": api.context, + "application": api.application, + "applications": api.applications, + "source_id": api.source_id, + "sources": api.sources, + "ambilight_styles": api.ambilight_styles, + "ambilight_topology": api.ambilight_topology, + "ambilight_current_configuration": api.ambilight_current_configuration, + "ambilight_mode_raw": api.ambilight_mode_raw, + "ambilight_modes": api.ambilight_modes, + "ambilight_power_raw": api.ambilight_power_raw, + "ambilight_power": api.ambilight_power, + "ambilight_cached": api.ambilight_cached, + "ambilight_measured": api.ambilight_measured, + "ambilight_processed": api.ambilight_processed, + "screenstate": api.screenstate, + "on": api.on, + "channel": api.channel, + "channels": api.channels, + "channel_lists": api.channel_lists, + "favorite_lists": api.favorite_lists, + }, + } diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 60bc862406d9d..948ce8703a10d 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -5,5 +5,6 @@ "requirements": ["ha-philipsjs==2.7.6"], "codeowners": ["@elupus"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["haphilipsjs"] } diff --git a/homeassistant/components/philips_js/translations/el.json b/homeassistant/components/philips_js/translations/el.json index e10c6a8bbcc65..ef134dc9064ab 100644 --- a/homeassistant/components/philips_js/translations/el.json +++ b/homeassistant/components/philips_js/translations/el.json @@ -1,7 +1,13 @@ { "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": { - "pairing_failure": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7: {error_id}" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf PIN", + "pairing_failure": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7: {error_id}", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "pair": { @@ -13,7 +19,8 @@ }, "user": { "data": { - "api_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 API" + "api_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" } } } @@ -22,5 +29,14 @@ "trigger_type": { "turn_on": "\u0396\u03b7\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "\u0395\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/pt-BR.json b/homeassistant/components/philips_js/translations/pt-BR.json new file mode 100644 index 0000000000000..a4da1d92ed683 --- /dev/null +++ b/homeassistant/components/philips_js/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_pin": "PIN inv\u00e1lido", + "pairing_failure": "N\u00e3o foi poss\u00edvel parear: {error_id}", + "unknown": "Erro inesperado" + }, + "step": { + "pair": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "Digite o PIN exibido na sua TV", + "title": "Par" + }, + "user": { + "data": { + "api_version": "Vers\u00e3o da API", + "host": "Nome do host" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Dispositivo for solicitado para ligar" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Permitir o uso do servi\u00e7o de notifica\u00e7\u00e3o de dados." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/sk.json b/homeassistant/components/philips_js/translations/sk.json index 8332248a6c6f4..623af989f3078 100644 --- a/homeassistant/components/philips_js/translations/sk.json +++ b/homeassistant/components/philips_js/translations/sk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Zariadenie je u\u017e nastaven\u00e9" + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json index 4e12fcd009cae..3ea322a6c635d 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ b/homeassistant/components/pi4ioe5v9xxxx/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", "requirements": ["pi4ioe5v9xxxx==0.0.2"], "codeowners": ["@antonverburg"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pi4ioe5v9xxxx", "smbus2"] } diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 28ceb8e6c45bc..cca92d9bcecd1 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -5,5 +5,6 @@ "requirements": ["hole==0.7.0"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["hole"] } diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index 5c1ab6ee04d7d..b6aa0fe536577 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -1,9 +1,27 @@ { "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" + }, "step": { + "api_key": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, "user": { "data": { - "statistics_only": "\u039c\u03cc\u03bd\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "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", + "statistics_only": "\u039c\u03cc\u03bd\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1", + "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" } } } diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index 8b7cd1004eaa2..3de821afe8d98 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -1,20 +1,26 @@ { "config": { "abort": { - "already_configured": "Servi\u00e7o j\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" }, "step": { + "api_key": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { - "api_key": "Chave de API", - "host": "Endere\u00e7o (IP)", + "api_key": "Chave da API", + "host": "Nome do host", "location": "Localiza\u00e7\u00e3o", "name": "Nome", "port": "Porta", - "ssl": "Usar SSL", + "ssl": "Usar um certificado SSL", + "statistics_only": "Somente estat\u00edsticas", "verify_ssl": "Verifique o certificado SSL" } } diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json new file mode 100644 index 0000000000000..4d37397c80038 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "step": { + "api_key": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "location": "Umiestnenie", + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index 09a1d52428333..c2d48ca94152a 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN @@ -71,8 +72,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + async def async_step_reauth(self, _): + """Perform the re-auth step upon an API authentication error.""" + return await self.async_step_user() + async def async_step_user(self, user_input=None): - """Handle the initial step.""" + """Handle the authentication step, this is the generic step for both `step_user` and `step_reauth`.""" if user_input is None: return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA @@ -90,17 +95,25 @@ async def async_step_user(self, user_input=None): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - # Set the unique id and abort if it already exists - await self.async_set_unique_id(info["unique_id"]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=info["title"], - data={ - CONF_ACCESS_TOKEN: auth_token, - CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], - }, - ) + data = { + CONF_ACCESS_TOKEN: auth_token, + CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], + } + existing_entry = await self.async_set_unique_id(info["unique_id"]) + + # Abort if we're adding a new config and the unique id is already in use, else create the entry + if self.source != SOURCE_REAUTH: + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=data) + + # In case of re-auth, only continue if an exiting account exists with the same unique id + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + # Set the error because the account is different + errors["base"] = "different_account" return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index a97d46e0ad07a..f33f58c0eb908 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -22,6 +22,7 @@ ADDRESS = "address" CART_DATA = "cart_data" SLOT_DATA = "slot_data" +NEXT_DELIVERY_DATA = "next_delivery_data" LAST_ORDER_DATA = "last_order_data" SENSOR_CART_ITEMS_COUNT = "cart_items_count" @@ -33,18 +34,22 @@ SENSOR_LAST_ORDER_SLOT_START = "last_order_slot_start" SENSOR_LAST_ORDER_SLOT_END = "last_order_slot_end" SENSOR_LAST_ORDER_STATUS = "last_order_status" -SENSOR_LAST_ORDER_ETA_START = "last_order_eta_start" -SENSOR_LAST_ORDER_ETA_END = "last_order_eta_end" SENSOR_LAST_ORDER_MAX_ORDER_TIME = "last_order_max_order_time" SENSOR_LAST_ORDER_DELIVERY_TIME = "last_order_delivery_time" SENSOR_LAST_ORDER_TOTAL_PRICE = "last_order_total_price" +SENSOR_NEXT_DELIVERY_ETA_START = "next_delivery_eta_start" +SENSOR_NEXT_DELIVERY_ETA_END = "next_delivery_eta_end" +SENSOR_NEXT_DELIVERY_SLOT_START = "next_delivery_slot_start" +SENSOR_NEXT_DELIVERY_SLOT_END = "next_delivery_slot_end" @dataclass class PicnicRequiredKeysMixin: """Mixin for required keys.""" - data_type: Literal["cart_data", "slot_data", "last_order_data"] + data_type: Literal[ + "cart_data", "slot_data", "next_delivery_data", "last_order_data" + ] value_fn: Callable[[Any], StateType | datetime] @@ -130,26 +135,6 @@ class PicnicSensorEntityDescription(SensorEntityDescription, PicnicRequiredKeysM data_type="last_order_data", value_fn=lambda last_order: last_order.get("status"), ), - PicnicSensorEntityDescription( - key=SENSOR_LAST_ORDER_ETA_START, - device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-start", - entity_registry_enabled_default=True, - data_type="last_order_data", - value_fn=lambda last_order: dt_util.parse_datetime( - str(last_order.get("eta", {}).get("start")) - ), - ), - PicnicSensorEntityDescription( - key=SENSOR_LAST_ORDER_ETA_END, - device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-end", - entity_registry_enabled_default=True, - data_type="last_order_data", - value_fn=lambda last_order: dt_util.parse_datetime( - str(last_order.get("eta", {}).get("end")) - ), - ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_MAX_ORDER_TIME, device_class=SensorDeviceClass.TIMESTAMP, @@ -177,4 +162,42 @@ class PicnicSensorEntityDescription(SensorEntityDescription, PicnicRequiredKeysM data_type="last_order_data", value_fn=lambda last_order: last_order.get("total_price", 0) / 100, ), + PicnicSensorEntityDescription( + key=SENSOR_NEXT_DELIVERY_ETA_START, + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:clock-start", + entity_registry_enabled_default=True, + data_type="next_delivery_data", + value_fn=lambda next_delivery: dt_util.parse_datetime( + str(next_delivery.get("eta", {}).get("start")) + ), + ), + PicnicSensorEntityDescription( + key=SENSOR_NEXT_DELIVERY_ETA_END, + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:clock-end", + entity_registry_enabled_default=True, + data_type="next_delivery_data", + value_fn=lambda next_delivery: dt_util.parse_datetime( + str(next_delivery.get("eta", {}).get("end")) + ), + ), + PicnicSensorEntityDescription( + key=SENSOR_NEXT_DELIVERY_SLOT_START, + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:calendar-start", + data_type="next_delivery_data", + value_fn=lambda next_delivery: dt_util.parse_datetime( + str(next_delivery.get("slot", {}).get("window_start")) + ), + ), + PicnicSensorEntityDescription( + key=SENSOR_NEXT_DELIVERY_SLOT_END, + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:calendar-end", + data_type="next_delivery_data", + value_fn=lambda next_delivery: dt_util.parse_datetime( + str(next_delivery.get("slot", {}).get("window_end")) + ), + ), ) diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 71a6559975c19..9f387858e5fdb 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, SLOT_DATA +from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, NEXT_DELIVERY_DATA, SLOT_DATA class PicnicUpdateCoordinator(DataUpdateCoordinator): @@ -59,18 +59,17 @@ async def _async_update_data(self) -> dict: def fetch_data(self): """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" # Fetch from the API and pre-process the data - cart = self.picnic_api_client.get_cart() - - if not cart: + if not (cart := self.picnic_api_client.get_cart()): raise UpdateFailed("API response doesn't contain expected data.") - last_order = self._get_last_order() + next_delivery, last_order = self._get_order_data() slot_data = self._get_slot_data(cart) return { ADDRESS: self._get_address(), CART_DATA: cart, SLOT_DATA: slot_data, + NEXT_DELIVERY_DATA: next_delivery, LAST_ORDER_DATA: last_order, } @@ -98,47 +97,55 @@ def _get_slot_data(cart: dict) -> dict: return {} - def _get_last_order(self) -> dict: + def _get_order_data(self) -> tuple[dict, dict]: """Get data of the last order from the list of deliveries.""" # Get the deliveries deliveries = self.picnic_api_client.get_deliveries(summary=True) # Determine the last order and return an empty dict if there is none try: - last_order = copy.deepcopy(deliveries[0]) - except KeyError: - return {} + # Filter on status CURRENT and select the last on the list which is the first one to be delivered + # Make a deepcopy because some references are local + next_deliveries = list( + filter(lambda d: d["status"] == "CURRENT", deliveries) + ) + next_delivery = ( + copy.deepcopy(next_deliveries[-1]) if next_deliveries else {} + ) + last_order = copy.deepcopy(deliveries[0]) if deliveries else {} + except (KeyError, TypeError): + # A KeyError or TypeError indicate that the response contains unexpected data + return {}, {} - # Get the position details if the order is not delivered yet + # Get the next order's position details if there is an undelivered order delivery_position = {} - if not last_order.get("delivery_time"): + if next_delivery and not next_delivery.get("delivery_time"): try: delivery_position = self.picnic_api_client.get_delivery_position( - last_order["delivery_id"] + next_delivery["delivery_id"] ) except ValueError: # No information yet can mean an empty response pass # Determine the ETA, if available, the one from the delivery position API is more precise - # but it's only available shortly before the actual delivery. - last_order["eta"] = delivery_position.get( - "eta_window", last_order.get("eta2", {}) + # but, it's only available shortly before the actual delivery. + next_delivery["eta"] = delivery_position.get( + "eta_window", next_delivery.get("eta2", {}) ) + if "eta2" in next_delivery: + del next_delivery["eta2"] # Determine the total price by adding up the total price of all sub-orders total_price = 0 for order in last_order.get("orders", []): total_price += order.get("total_price", 0) - - # Sanitise the object last_order["total_price"] = total_price + + # Make sure delivery_time is a dict last_order.setdefault("delivery_time", {}) - if "eta2" in last_order: - del last_order["eta2"] - # Make a copy because some references are local - return last_order + return next_delivery, last_order @callback def _update_auth_token(self): diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json index 757f2ef24ad8b..54bcab8e3fd8a 100644 --- a/homeassistant/components/picnic/manifest.json +++ b/homeassistant/components/picnic/manifest.json @@ -5,5 +5,6 @@ "iot_class": "cloud_polling", "documentation": "https://www.home-assistant.io/integrations/picnic", "requirements": ["python-picnic-api==1.1.0"], - "codeowners": ["@corneyl"] + "codeowners": ["@corneyl"], + "loggers": ["python_picnic_api"] } \ No newline at end of file diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json index 7fbd5e9bef679..9eb51b2fd2a4a 100644 --- a/homeassistant/components/picnic/strings.json +++ b/homeassistant/components/picnic/strings.json @@ -12,10 +12,12 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "different_account": "Account should be the same as used for setting up the integration" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json new file mode 100644 index 0000000000000..c0ccf23f5b5c3 --- /dev/null +++ b/homeassistant/components/picnic/translations/bg.json @@ -0,0 +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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ca.json b/homeassistant/components/picnic/translations/ca.json index c81d180aef006..83c0b75f9d3c0 100644 --- a/homeassistant/components/picnic/translations/ca.json +++ b/homeassistant/components/picnic/translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "different_account": "El compte ha de ser el mateix que s'utilitza per configurar la integraci\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/picnic/translations/cs.json b/homeassistant/components/picnic/translations/cs.json index dc27752e93594..988637d097729 100644 --- a/homeassistant/components/picnic/translations/cs.json +++ b/homeassistant/components/picnic/translations/cs.json @@ -1,7 +1,8 @@ { "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", @@ -11,6 +12,7 @@ "step": { "user": { "data": { + "country_code": "K\u00f3d zem\u011b", "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } diff --git a/homeassistant/components/picnic/translations/de.json b/homeassistant/components/picnic/translations/de.json index 1a11e00664cef..65b10f61df3aa 100644 --- a/homeassistant/components/picnic/translations/de.json +++ b/homeassistant/components/picnic/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "different_account": "Das Konto sollte dasselbe sein, das f\u00fcr die Einrichtung der Integration verwendet wurde.", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/picnic/translations/el.json b/homeassistant/components/picnic/translations/el.json index f931aa6f3c00f..ba974da622582 100644 --- a/homeassistant/components/picnic/translations/el.json +++ b/homeassistant/components/picnic/translations/el.json @@ -1,9 +1,21 @@ { "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", + "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \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", + "different_account": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03af\u03b4\u03b9\u03bf\u03c2 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\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": { "user": { "data": { - "country_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03ce\u03c1\u03b1\u03c2" + "country_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03ce\u03c1\u03b1\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/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json index c7097df12a961..06b3018f88e78 100644 --- a/homeassistant/components/picnic/translations/en.json +++ b/homeassistant/components/picnic/translations/en.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", + "different_account": "Account should be the same as used for setting up the integration", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 848f72e62d6d2..f7a170871efae 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "Reauntenticaci\u00f3n exitosa" }, "error": { "cannot_connect": "No se pudo conectar", + "different_account": "La cuenta debe ser la misma que se utiliz\u00f3 para configurar la integraci\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/picnic/translations/et.json b/homeassistant/components/picnic/translations/et.json index 11fc0f1fe881e..41f5018079c9d 100644 --- a/homeassistant/components/picnic/translations/et.json +++ b/homeassistant/components/picnic/translations/et.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "T\u00f5rge \u00fchendamisel", + "different_account": "Konto peab olema sama mida kasutati sidumise seadistamisel", "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu t\u00f5rge" }, diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 03b5566566f09..794a33ffe754e 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", + "different_account": "Le compte doit \u00eatre le m\u00eame que celui utilis\u00e9 pour configurer l'int\u00e9gration", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/picnic/translations/he.json b/homeassistant/components/picnic/translations/he.json index f668538909b4a..856ac220d6424 100644 --- a/homeassistant/components/picnic/translations/he.json +++ b/homeassistant/components/picnic/translations/he.json @@ -1,10 +1,12 @@ { "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": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "different_account": "\u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05d6\u05d4\u05d4 \u05dc\u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1", "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" }, diff --git a/homeassistant/components/picnic/translations/hu.json b/homeassistant/components/picnic/translations/hu.json index c70dcca026050..3841b7ddbafb0 100644 --- a/homeassistant/components/picnic/translations/hu.json +++ b/homeassistant/components/picnic/translations/hu.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", + "different_account": "A fi\u00f3knak meg kell egyeznie az integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1hoz haszn\u00e1lt fi\u00f3kkal", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba" }, diff --git a/homeassistant/components/picnic/translations/id.json b/homeassistant/components/picnic/translations/id.json index 819125c690907..db97b991f6f6d 100644 --- a/homeassistant/components/picnic/translations/id.json +++ b/homeassistant/components/picnic/translations/id.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", + "different_account": "Akun harus sama dengan yang digunakan untuk menyiapkan integrasi", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/picnic/translations/it.json b/homeassistant/components/picnic/translations/it.json index e77faae817d10..209b6d6fdb968 100644 --- a/homeassistant/components/picnic/translations/it.json +++ b/homeassistant/components/picnic/translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", + "different_account": "L'account deve essere lo stesso utilizzato per impostare l'integrazione", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index 5379949aa9671..194cffd7e6a3b 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\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" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/picnic/translations/nb.json b/homeassistant/components/picnic/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/picnic/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json index 210eebdf35717..dc040fc03d027 100644 --- a/homeassistant/components/picnic/translations/nl.json +++ b/homeassistant/components/picnic/translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", + "different_account": "Account moet dezelfde zijn als die gebruikt is voor het opzetten van de integratie", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json index 45e3bcbb5487b..ffd38bce70515 100644 --- a/homeassistant/components/picnic/translations/no.json +++ b/homeassistant/components/picnic/translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", + "different_account": "Kontoen skal v\u00e6re den samme som brukes til \u00e5 sette opp integrasjonen", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/picnic/translations/pl.json b/homeassistant/components/picnic/translations/pl.json index c278f29d13cf5..abf8a4f9469c0 100644 --- a/homeassistant/components/picnic/translations/pl.json +++ b/homeassistant/components/picnic/translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "different_account": "Konto powinno by\u0107 takie samo jak przy konfigurowaniu integracji.", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/picnic/translations/pt-BR.json b/homeassistant/components/picnic/translations/pt-BR.json new file mode 100644 index 0000000000000..b864d13923d46 --- /dev/null +++ b/homeassistant/components/picnic/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "different_account": "A conta deve ser a mesma usada para configurar a integra\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "country_code": "C\u00f3digo do pa\u00eds", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ru.json b/homeassistant/components/picnic/translations/ru.json index e754faf8a0e96..9d7a7fbdb23c0 100644 --- a/homeassistant/components/picnic/translations/ru.json +++ b/homeassistant/components/picnic/translations/ru.json @@ -1,10 +1,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." + "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.", + "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.", + "different_account": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u043e\u0439 \u0436\u0435, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u0434\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", "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." }, diff --git a/homeassistant/components/picnic/translations/sk.json b/homeassistant/components/picnic/translations/sk.json new file mode 100644 index 0000000000000..1c63a1923bdcd --- /dev/null +++ b/homeassistant/components/picnic/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "country_code": "K\u00f3d krajiny" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/tr.json b/homeassistant/components/picnic/translations/tr.json index 242b4ae4e6a97..b689f65ff9631 100644 --- a/homeassistant/components/picnic/translations/tr.json +++ b/homeassistant/components/picnic/translations/tr.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "different_account": "Hesap, entegrasyonu ayarlamak i\u00e7in kullan\u0131lanla ayn\u0131 olmal\u0131d\u0131r", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/picnic/translations/zh-Hant.json b/homeassistant/components/picnic/translations/zh-Hant.json index 2f72809d4fe91..a82f4ace04de7 100644 --- a/homeassistant/components/picnic/translations/zh-Hant.json +++ b/homeassistant/components/picnic/translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "different_account": "\u5e33\u865f\u5fc5\u9808\u76f8\u540c\u4ee5\u8a2d\u5b9a\u6574\u5408", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index e7173df21d9f2..e8357caeb64ae 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": ["pilight==0.1.1"], "codeowners": ["@trekky12"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pilight"] } diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index d25d0fc731e21..4aec8dbee1ab4 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -5,5 +5,6 @@ "codeowners": [], "requirements": ["icmplib==3.0"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["icmplib"] } diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index ea07cc5d85a0b..5c9436433ed2b 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": ["pypjlink2==1.2.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pypjlink"] } diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 183eb98e21376..19c935338676e 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -180,7 +180,7 @@ async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platfo return unloaded -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index 99453f21d459a..ddb3d4474a3c5 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -7,5 +7,6 @@ "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], "requirements": ["pyplaato==0.0.15"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pyplaato"] } diff --git a/homeassistant/components/plaato/translations/bg.json b/homeassistant/components/plaato/translations/bg.json index d9dd41dbb804c..c31f1f8a14a78 100644 --- a/homeassistant/components/plaato/translations/bg.json +++ b/homeassistant/components/plaato/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json index 06aa27e5b3721..4c2959bafa242 100644 --- a/homeassistant/components/plaato/translations/ca.json +++ b/homeassistant/components/plaato/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El compte ja est\u00e0 configurat", + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 29e3ebe979085..d3e39738b0228 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/plaato/translations/el.json b/homeassistant/components/plaato/translations/el.json index 9be7164e051de..8813a4d71bf8a 100644 --- a/homeassistant/components/plaato/translations/el.json +++ b/homeassistant/components/plaato/translations/el.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "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", + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u03a4\u03bf Plaato {device_type} \u03bc\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 **{device_name}** \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1!" }, "error": { "invalid_webhook_device": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 webhook. \u0395\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf Airlock", @@ -21,10 +27,28 @@ "data": { "device_name": "\u039f\u03bd\u03bf\u03bc\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2", "device_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Plaato" - } + }, + "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;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd Plaato" + }, + "webhook": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Plaato Airlock.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: \n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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.", + "title": "Webhook \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)" + }, + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Plaato" }, "webhook": { - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Plaato Airlock.\n\n\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2:\n\n- URL: `{webhook_url}`\n- \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: \n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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." + "description": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 Webhook: \n\n - URL: `{webhook_url}`\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n\n", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/en.json b/homeassistant/components/plaato/translations/en.json index 1217eb53d6eac..0eba3a9431023 100644 --- a/homeassistant/components/plaato/translations/en.json +++ b/homeassistant/components/plaato/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Account is already configured", + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json index fb4325581dd37..f563646753ecf 100644 --- a/homeassistant/components/plaato/translations/et.json +++ b/homeassistant/components/plaato/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Kasutaja on juba seadistatud", + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json index 3bac269eaf92d..370796c18f397 100644 --- a/homeassistant/components/plaato/translations/fr.json +++ b/homeassistant/components/plaato/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/plaato/translations/he.json b/homeassistant/components/plaato/translations/he.json index 014783d743188..28375b45c2cd5 100644 --- a/homeassistant/components/plaato/translations/he.json +++ b/homeassistant/components/plaato/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." }, diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index a25c0c356720d..245b0dc2a0c39 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/plaato/translations/id.json b/homeassistant/components/plaato/translations/id.json index 989bb38bcaf0f..99783fd4a630b 100644 --- a/homeassistant/components/plaato/translations/id.json +++ b/homeassistant/components/plaato/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json index 722d1c5c34c46..26fe409bfc2d6 100644 --- a/homeassistant/components/plaato/translations/it.json +++ b/homeassistant/components/plaato/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 8b3f030b72f45..0842ca6da74fa 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/plaato/translations/nb.json b/homeassistant/components/plaato/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/plaato/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index 7dc3eaf6fb7e0..83e2874ed7327 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json index 7039662468ef2..8065c4fb471af 100644 --- a/homeassistant/components/plaato/translations/no.json +++ b/homeassistant/components/plaato/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index ddfb779ea2e7d..af94e340408db 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/plaato/translations/pt-BR.json b/homeassistant/components/plaato/translations/pt-BR.json index 01c296e59ac8f..4b57d3f984e3d 100644 --- a/homeassistant/components/plaato/translations/pt-BR.json +++ b/homeassistant/components/plaato/translations/pt-BR.json @@ -1,12 +1,54 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { - "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook na Plaato Airlock.\n\nPreencha as seguintes informa\u00e7\u00f5es:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nVeja [a documenta\u00e7\u00e3o]({docs_url}) para mais detalhes." + "default": "Seu Plaato {device_type} com o nome **{device_name}** foi configurado com sucesso!" + }, + "error": { + "invalid_webhook_device": "Voc\u00ea selecionou um dispositivo que n\u00e3o suporta o envio de dados para um webhook. Est\u00e1 dispon\u00edvel apenas para o Airlock", + "no_api_method": "Voc\u00ea precisa adicionar um token de autentica\u00e7\u00e3o ou selecionar webhook", + "no_auth_token": "Voc\u00ea precisa adicionar um token de autentica\u00e7\u00e3o" }, "step": { + "api_method": { + "data": { + "token": "Cole o token de autentica\u00e7\u00e3o aqui", + "use_webhook": "Usar webhook" + }, + "description": "Para poder consultar a API, \u00e9 necess\u00e1rio um `auth_token`, que pode ser obtido seguindo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instru\u00e7\u00f5es \n\n Dispositivo selecionado: ** {device_type} ** \n\n Se voc\u00ea preferir usar o m\u00e9todo de webhook integrado (somente Airlock), marque a caixa abaixo e deixe o token de autentica\u00e7\u00e3o em branco", + "title": "Selecione o m\u00e9todo de API" + }, "user": { - "description": "Tens a certeza que queres montar a Plaato Airlock?", + "data": { + "device_name": "D\u00ea um nome ao seu dispositivo", + "device_type": "Tipo de dispositivo Plaato" + }, + "description": "Deseja iniciar a configura\u00e7\u00e3o?", "title": "Configurar o Plaato Webhook" + }, + "webhook": { + "description": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Plaato Airlock. \n\nPreencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) para obter mais detalhes.", + "title": "Webhook para usar" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Intervalo de atualiza\u00e7\u00e3o (minutos)" + }, + "description": "Defina o intervalo de atualiza\u00e7\u00e3o (minutos)", + "title": "Op\u00e7\u00f5es para Plaato" + }, + "webhook": { + "description": "Informa\u00e7\u00f5es do webhook: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST \n\n", + "title": "Op\u00e7\u00f5es para a Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json index 9ff1977dc53d3..f0963321cab1b 100644 --- a/homeassistant/components/plaato/translations/ru.json +++ b/homeassistant/components/plaato/translations/ru.json @@ -2,6 +2,7 @@ "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.", + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json index 579617127ac80..21f2bddcc353c 100644 --- a/homeassistant/components/plaato/translations/tr.json +++ b/homeassistant/components/plaato/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/plaato/translations/uk.json b/homeassistant/components/plaato/translations/uk.json index a4f7de7c6be4e..6e740a68cdba7 100644 --- a/homeassistant/components/plaato/translations/uk.json +++ b/homeassistant/components/plaato/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index 26d7b728771b7..8ffa238d816fd 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/plant/translations/pt-BR.json b/homeassistant/components/plant/translations/pt-BR.json index 09b88d2578b97..25b0fe796dac4 100644 --- a/homeassistant/components/plant/translations/pt-BR.json +++ b/homeassistant/components/plant/translations/pt-BR.json @@ -1,9 +1,9 @@ { "state": { "_": { - "ok": "Ok", + "ok": "OK", "problem": "Problema" } }, - "title": "Planta" + "title": "Monitor de Planta" } \ No newline at end of file diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 41b7f0d0afd18..df3a4b8cd11a1 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -25,10 +25,12 @@ async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request from homeassistant.helpers.typing import ConfigType from .const import ( + CLIENT_SCAN_INTERVAL, CONF_SERVER, CONF_SERVER_IDENTIFIER, DISPATCHERS, @@ -48,6 +50,7 @@ from .media_browser import browse_media from .server import PlexServer from .services import async_setup_services +from .view import PlexImageView _LOGGER = logging.getLogger(__package__) @@ -84,6 +87,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_setup_services(hass) + hass.http.register_view(PlexImageView()) + gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM() def gdm_scan(): @@ -244,6 +249,19 @@ def get_plex_account(plex_server): await hass.async_add_executor_job(get_plex_account, plex_server) + @callback + def scheduled_client_scan(_): + _LOGGER.debug("Scheduled scan for new clients on %s", plex_server.friendly_name) + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + entry.async_on_unload( + async_track_time_interval( + hass, + scheduled_client_scan, + CLIENT_SCAN_INTERVAL, + ) + ) + return True @@ -265,7 +283,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_options_updated(hass, entry): +async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Triggered by config entry options updates.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] diff --git a/homeassistant/components/plex/button.py b/homeassistant/components/plex/button.py new file mode 100644 index 0000000000000..23e8ec103e923 --- /dev/null +++ b/homeassistant/components/plex/button.py @@ -0,0 +1,53 @@ +"""Representation of Plex buttons.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + CONF_SERVER, + CONF_SERVER_IDENTIFIER, + DOMAIN, + PLEX_UPDATE_PLATFORMS_SIGNAL, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Plex button from config entry.""" + server_id: str = config_entry.data[CONF_SERVER_IDENTIFIER] + server_name: str = config_entry.data[CONF_SERVER] + async_add_entities([PlexScanClientsButton(server_id, server_name)]) + + +class PlexScanClientsButton(ButtonEntity): + """Representation of a scan_clients button entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, server_id: str, server_name: str) -> None: + """Initialize a scan_clients Plex button entity.""" + self.server_id = server_id + self._attr_name = f"Scan Clients ({server_name})" + self._attr_unique_id = f"plex-scan_clients-{self.server_id}" + + async def async_press(self) -> None: + """Press the button.""" + async_dispatcher_send( + self.hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(self.server_id) + ) + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + return DeviceInfo( + identifiers={(DOMAIN, self.server_id)}, + manufacturer="Plex", + ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index ce28357a4b192..dea976f46dd56 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,6 @@ """Constants for the Plex component.""" +from datetime import timedelta + from homeassistant.const import Platform, __version__ DOMAIN = "plex" @@ -12,11 +14,12 @@ PLEXTV_THROTTLE = 60 +CLIENT_SCAN_INTERVAL = timedelta(minutes=10) DEBOUNCE_TIMEOUT = 1 DISPATCHERS = "dispatchers" GDM_DEBOUNCER = "gdm_debouncer" GDM_SCANNER = "gdm_scanner" -PLATFORMS = frozenset([Platform.MEDIA_PLAYER, Platform.SENSOR]) +PLATFORMS = frozenset([Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR]) PLATFORMS_COMPLETED = "platforms_completed" PLAYER_SOURCE = "player_source" SERVERS = "servers" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 2d45f1217a7a1..85a060ae7cd8c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,12 +4,13 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.9.2", + "plexapi==4.10.0", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], "zeroconf": ["_plexmediasvr._tcp.local."], "dependencies": ["http"], "codeowners": ["@jjlawren"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["plexapi", "plexwebsocket"] } diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 0bff5cfb5cd9f..115990861799a 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,4 +1,6 @@ """Support to interface with the Plex API.""" +from __future__ import annotations + import logging from homeassistant.components.media_player import BrowseMedia @@ -73,7 +75,15 @@ def item_payload(item, short_name=False): "can_expand": item.type in EXPANDABLES, } if hasattr(item, "thumbUrl"): - payload["thumbnail"] = item.thumbUrl + plex_server.thumbnail_cache.setdefault(str(item.ratingKey), item.thumbUrl) + if is_internal: + thumbnail = item.thumbUrl + else: + thumbnail = get_proxy_image_url( + plex_server.machine_identifier, + item.ratingKey, + ) + payload["thumbnail"] = thumbnail return BrowseMedia(**payload) @@ -321,3 +331,11 @@ def station_payload(station): can_play=True, can_expand=False, ) + + +def get_proxy_image_url( + server_id: str, + media_content_id: str, +) -> str: + """Generate an url for a Plex media browser image.""" + return f"/api/plex_image_proxy/{server_id}/{media_content_id}" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1ff58ed468d0c..9b8b0df14c794 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -585,17 +585,3 @@ async def async_browse_media(self, media_content_type=None, media_content_id=Non media_content_type, media_content_id, ) - - async def async_get_browse_image( - self, - media_content_type: str, - media_content_id: str, - media_image_id: str | None = None, - ) -> tuple[bytes | None, str | None]: - """Get media image from Plex server.""" - image_url = self.plex_server.thumbnail_cache.get(media_content_id) - if image_url: - result = await self._async_fetch_image(image_url) - return result - - return (None, None) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f1ac620f37906..b4dc5755a73a7 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -644,7 +644,10 @@ def lookup_media(self, media_type, **kwargs): _LOGGER.error("Must specify 'library_name' for this search") return None except NotFound: - _LOGGER.error("Library '%s' not found", library_name) + library_sections = [section.title for section in self.library.sections()] + _LOGGER.error( + "Library '%s' not found in %s", library_name, library_sections + ) return None return search_media(media_type, library_section, **kwargs) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 11d40dbab75a2..0433ba836cd8a 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -31,7 +31,10 @@ async def async_refresh_library_service(service_call: ServiceCall) -> None: await hass.async_add_executor_job(refresh_library, hass, service_call) async def async_scan_clients_service(_: ServiceCall) -> None: - _LOGGER.debug("Scanning for new Plex clients") + _LOGGER.warning( + "This service is deprecated in favor of the scan_clients button entity. " + "Service calls will still work for now but the service will be removed in a future release" + ) for server_id in hass.data[DOMAIN][SERVERS]: async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) diff --git a/homeassistant/components/plex/translations/cs.json b/homeassistant/components/plex/translations/cs.json index de85391a7d989..f5e7e538f8497 100644 --- a/homeassistant/components/plex/translations/cs.json +++ b/homeassistant/components/plex/translations/cs.json @@ -5,6 +5,7 @@ "already_configured": "Tento server Plex je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "token_request_timeout": "\u010casov\u00fd limit pro z\u00edsk\u00e1n\u00ed tokenu vypr\u0161el", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { @@ -34,6 +35,7 @@ "title": "Vyberte server Plex" }, "user": { + "description": "Pro propojen\u00ed Plex serveru, pokra\u010dujte na [plex.tv](https://plex.tv).", "title": "Plex Media Server" }, "user_advanced": { @@ -48,8 +50,10 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorovat nov\u00e9 spravovan\u00e9/sd\u00edlen\u00e9 u\u017eivatele", "ignore_plex_web_clients": "Ignorujte webov\u00e9 klienty Plex", - "monitored_users": "Sledovan\u00ed u\u017eivatel\u00e9" + "monitored_users": "Sledovan\u00ed u\u017eivatel\u00e9", + "use_episode_art": "Pou\u017e\u00edvat plak\u00e1ty epizod" }, "description": "Mo\u017enosti pro p\u0159ehr\u00e1va\u010de m\u00e9di\u00ed Plex" } diff --git a/homeassistant/components/plex/translations/el.json b/homeassistant/components/plex/translations/el.json index 721823efbc742..3f447b48d8633 100644 --- a/homeassistant/components/plex/translations/el.json +++ b/homeassistant/components/plex/translations/el.json @@ -3,22 +3,36 @@ "abort": { "all_configured": "\u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1" + "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", + "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "token_request_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "faulty_credentials": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf Token", + "host_or_token": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03b5\u03be\u03ae\u03c2: Host \u03ae Token", "no_servers": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03b9 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Plex", "not_found": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", "ssl_error": "\u0396\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\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", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "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" + }, "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Plex" }, "select_server": { "data": { "server": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2" - } + }, + "description": "\u0394\u03b9\u03b1\u03c4\u03af\u03b8\u03b5\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bf\u03bb\u03bb\u03bf\u03af \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd:", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Plex" }, "user": { "description": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf [plex.tv](https://plex.tv) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Plex.", @@ -37,7 +51,9 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03bd\u03ad\u03bf\u03c5\u03c2 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c5\u03c2/\u03ba\u03bf\u03b9\u03bd\u03cc\u03c7\u03c1\u03b7\u03c3\u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2", - "monitored_users": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2" + "ignore_plex_web_clients": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 Web Plex", + "monitored_users": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2", + "use_episode_art": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b5\u03be\u03ce\u03c6\u03c5\u03bb\u03bb\u03bf \u03b5\u03c0\u03b5\u03b9\u03c3\u03bf\u03b4\u03af\u03c9\u03bd" }, "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd Plex" } diff --git a/homeassistant/components/plex/translations/pt-BR.json b/homeassistant/components/plex/translations/pt-BR.json index eac953579c0f0..ea74d2b173b7f 100644 --- a/homeassistant/components/plex/translations/pt-BR.json +++ b/homeassistant/components/plex/translations/pt-BR.json @@ -1,23 +1,48 @@ { "config": { + "abort": { + "all_configured": "Todos os servidores vinculados j\u00e1 configurados", + "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "token_request_timeout": "Tempo limite de obten\u00e7\u00e3o do token", + "unknown": "Erro inesperado" + }, "error": { + "faulty_credentials": "Falha na autoriza\u00e7\u00e3o, verifique o token", + "host_or_token": "Deve fornecer pelo menos um Host ou Token", + "no_servers": "Nenhum servidor vinculado \u00e0 conta Plex", + "not_found": "Servidor Plex n\u00e3o encontrado", "ssl_error": "Problema no certificado SSL" }, "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { + "host": "Nome do host", "port": "Porta", - "ssl": "Usar SSL", + "ssl": "Usar um certificado SSL", "token": "Token (Opcional)", "verify_ssl": "Verifique o certificado SSL" }, "title": "Configura\u00e7\u00e3o manual do Plex" }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "V\u00e1rios servidores dispon\u00edveis, selecione um:", + "title": "Selecione servidor Plex" + }, + "user": { + "description": "Continue para [plex.tv](https://plex.tv) para vincular um servidor Plex.", + "title": "Plex Media Server" + }, "user_advanced": { "data": { "setup_method": "M\u00e9todo de configura\u00e7\u00e3o" - } + }, + "title": "Plex Media Server" } } }, @@ -25,6 +50,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorar novos usu\u00e1rios gerenciados/compartilhados", + "ignore_plex_web_clients": "Ignorar clientes Web Plex", + "monitored_users": "Usu\u00e1rios monitorados", "use_episode_art": "Usar arte epis\u00f3dio" }, "description": "Op\u00e7\u00f5es para Plex Media Players" diff --git a/homeassistant/components/plex/translations/sk.json b/homeassistant/components/plex/translations/sk.json new file mode 100644 index 0000000000000..68438cbdfb0a2 --- /dev/null +++ b/homeassistant/components/plex/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "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", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "manual_setup": { + "data": { + "port": "Port", + "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/plex/view.py b/homeassistant/components/plex/view.py new file mode 100644 index 0000000000000..3cb7d40b2def0 --- /dev/null +++ b/homeassistant/components/plex/view.py @@ -0,0 +1,48 @@ +"""Implement a view to provide proxied Plex thumbnails to the media browser.""" +from __future__ import annotations + +from http import HTTPStatus +import logging + +from aiohttp import web +from aiohttp.hdrs import CACHE_CONTROL +from aiohttp.typedefs import LooseHeaders + +from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.media_player import async_fetch_image + +from .const import DOMAIN as PLEX_DOMAIN, SERVERS + +_LOGGER = logging.getLogger(__name__) + + +class PlexImageView(HomeAssistantView): + """Media player view to serve a Plex image.""" + + name = "api:plex:image" + url = "/api/plex_image_proxy/{server_id}/{media_content_id}" + + async def get( # pylint: disable=no-self-use + self, + request: web.Request, + server_id: str, + media_content_id: str, + ) -> web.Response: + """Start a get request.""" + if not request[KEY_AUTHENTICATED]: + return web.Response(status=HTTPStatus.UNAUTHORIZED) + + hass = request.app["hass"] + if (server := hass.data[PLEX_DOMAIN][SERVERS].get(server_id)) is None: + return web.Response(status=HTTPStatus.NOT_FOUND) + + if (image_url := server.thumbnail_cache.get(media_content_id)) is None: + return web.Response(status=HTTPStatus.NOT_FOUND) + + data, content_type = await async_fetch_image(_LOGGER, hass, image_url) + + if data is None: + return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE) + + headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"} + return web.Response(body=data, content_type=content_type, headers=headers) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index bf72d9edc31e4..129f4faef923d 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -1,30 +1,77 @@ """Plugwise Binary Sensor component for Home Assistant.""" -import logging +from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - COORDINATOR, - DOMAIN, - FLAME_ICON, - FLOW_OFF_ICON, - FLOW_ON_ICON, - IDLE_ICON, - NO_NOTIFICATION_ICON, - NOTIFICATION_ICON, -) -from .gateway import SmileGateway +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity -BINARY_SENSOR_MAP = { - "dhw_state": ["Domestic Hot Water State", None], - "slave_boiler_state": ["Secondary Heater Device State", None], -} SEVERITIES = ["other", "info", "warning", "error"] -_LOGGER = logging.getLogger(__name__) + +@dataclass +class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Plugwise binary sensor entity.""" + + icon_off: str | None = None + + +BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( + PlugwiseBinarySensorEntityDescription( + key="dhw_state", + name="DHW State", + icon="mdi:water-pump", + icon_off="mdi:water-pump-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + PlugwiseBinarySensorEntityDescription( + key="flame_state", + name="Flame State", + icon="mdi:fire", + icon_off="mdi:fire-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + PlugwiseBinarySensorEntityDescription( + key="heating_state", + name="Heating", + icon="mdi:radiator", + icon_off="mdi:radiator-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + PlugwiseBinarySensorEntityDescription( + key="cooling_state", + name="Cooling", + icon="mdi:snowflake", + icon_off="mdi:snowflake-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + PlugwiseBinarySensorEntityDescription( + key="slave_boiler_state", + name="Secondary Boiler State", + icon="mdi:fire", + icon_off="mdi:circle-off-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + PlugwiseBinarySensorEntityDescription( + key="plugwise_notification", + name="Plugwise Notification", + icon="mdi:mailbox-up-outline", + icon_off="mdi:mailbox-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) async def async_setup_entry( @@ -33,143 +80,73 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Smile binary_sensors from a config entry.""" - api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] - - entities: list[BinarySensorEntity] = [] - is_thermostat = api.single_master_thermostat() - - all_devices = api.get_all_devices() - for dev_id, device_properties in all_devices.items(): - - if device_properties["class"] == "heater_central": - data = api.get_device_data(dev_id) - for binary_sensor in BINARY_SENSOR_MAP: - if binary_sensor not in data: - continue - - entities.append( - PwBinarySensor( - api, - coordinator, - device_properties["name"], - dev_id, - binary_sensor, - ) - ) + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + entities: list[PlugwiseBinarySensorEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in BINARY_SENSORS: + if description.key not in device and ( + "binary_sensors" not in device + or description.key not in device["binary_sensors"] + ): + continue - if device_properties["class"] == "gateway" and is_thermostat is not None: entities.append( - PwNotifySensor( - api, + PlugwiseBinarySensorEntity( coordinator, - device_properties["name"], - dev_id, - "plugwise_notification", + device_id, + description, ) ) - - async_add_entities(entities, True) + async_add_entities(entities) -class SmileBinarySensor(SmileGateway): +class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): """Represent Smile Binary Sensors.""" - def __init__(self, api, coordinator, name, dev_id, binary_sensor): - """Initialise the binary_sensor.""" - super().__init__(api, coordinator, name, dev_id) - - self._binary_sensor = binary_sensor + entity_description: PlugwiseBinarySensorEntityDescription - self._icon = None - self._is_on = False - - if dev_id == self._api.heater_id: - self._entity_name = "Auxiliary" - - sensorname = binary_sensor.replace("_", " ").title() - self._name = f"{self._entity_name} {sensorname}" - - if dev_id == self._api.gateway_id: - self._entity_name = f"Smile {self._entity_name}" - - self._unique_id = f"{dev_id}-{binary_sensor}" - - @property - def icon(self): - """Return the icon of this entity.""" - return self._icon + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: PlugwiseBinarySensorEntityDescription, + ) -> None: + """Initialise the binary_sensor.""" + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" - return self._is_on - - @callback - def _async_process_data(self): - """Update the entity.""" - raise NotImplementedError - - -class PwBinarySensor(SmileBinarySensor, BinarySensorEntity): - """Representation of a Plugwise binary_sensor.""" - - @callback - def _async_process_data(self): - """Update the entity.""" - if not (data := self._api.get_device_data(self._dev_id)): - _LOGGER.error("Received no data for device %s", self._binary_sensor) - self.async_write_ha_state() - return - - if self._binary_sensor not in data: - self.async_write_ha_state() - return - - self._is_on = data[self._binary_sensor] - - if self._binary_sensor == "dhw_state": - self._icon = FLOW_ON_ICON if self._is_on else FLOW_OFF_ICON - if self._binary_sensor == "slave_boiler_state": - self._icon = FLAME_ICON if self._is_on else IDLE_ICON - - self.async_write_ha_state() - - -class PwNotifySensor(SmileBinarySensor, BinarySensorEntity): - """Representation of a Plugwise Notification binary_sensor.""" - - def __init__(self, api, coordinator, name, dev_id, binary_sensor): - """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id, binary_sensor) - - self._attributes = {} + if self.entity_description.key in self.device: + return self.device[self.entity_description.key] + return self.device["binary_sensors"].get(self.entity_description.key) @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @callback - def _async_process_data(self): - """Update the entity.""" - notify = self._api.notifications + def icon(self) -> str | None: + """Return the icon to use in the frontend, if any.""" + if (icon_off := self.entity_description.icon_off) and self.is_on is False: + return icon_off + return self.entity_description.icon - for severity in SEVERITIES: - self._attributes[f"{severity}_msg"] = [] - - self._is_on = False - self._icon = NO_NOTIFICATION_ICON - - if notify: - self._is_on = True - self._icon = NOTIFICATION_ICON + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + if self.entity_description.key != "plugwise_notification": + return None + attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES} + if notify := self.coordinator.data.gateway["notifications"]: for details in notify.values(): for msg_type, msg in details.items(): + msg_type = msg_type.lower() if msg_type not in SEVERITIES: msg_type = "other" + attrs[f"{msg_type}_msg"].append(msg) - self._attributes[f"{msg_type.lower()}_msg"].append(msg) - - self.async_write_ha_state() + return attrs diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 86f701ccb7de6..e22f12c35265f 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -1,7 +1,8 @@ """Plugwise Climate component for Home Assistant.""" -import logging +from __future__ import annotations -from plugwise.exceptions import PlugwiseException +from collections.abc import Mapping +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -9,32 +10,28 @@ CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - COORDINATOR, - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, - DOMAIN, - SCHEDULE_OFF, - SCHEDULE_ON, -) -from .gateway import SmileGateway - -HVAC_MODES_HEAT_ONLY = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] -HVAC_MODES_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity +from .util import plugwise_command -_LOGGER = logging.getLogger(__name__) +THERMOSTAT_CLASSES = [ + "thermostat", + "thermostatic_radiator_valve", + "zone_thermometer", + "zone_thermostat", +] async def async_setup_entry( @@ -43,234 +40,121 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Smile Thermostats from a config entry.""" - api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + PlugwiseClimateEntity(coordinator, device_id) + for device_id, device in coordinator.data.devices.items() + if device["class"] in THERMOSTAT_CLASSES + ) - entities = [] - thermostat_classes = [ - "thermostat", - "zone_thermostat", - "thermostatic_radiator_valve", - ] - all_devices = api.get_all_devices() - for dev_id, device_properties in all_devices.items(): - - if device_properties["class"] not in thermostat_classes: - continue - - thermostat = PwThermostat( - api, - coordinator, - device_properties["name"], - dev_id, - device_properties["location"], - device_properties["class"], - DEFAULT_MIN_TEMP, - DEFAULT_MAX_TEMP, - ) - - entities.append(thermostat) - - async_add_entities(entities, True) - - -class PwThermostat(SmileGateway, ClimateEntity): +class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__( - self, api, coordinator, name, dev_id, loc_id, model, min_temp, max_temp - ): + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id) - - self._api = api - self._loc_id = loc_id - self._model = model - self._min_temp = min_temp - self._max_temp = max_temp - - self._selected_schema = None - self._last_active_schema = None - self._preset_mode = None - self._presets = None - self._presets_list = None - self._heating_state = None - self._cooling_state = None - self._compressor_state = None - self._dhw_state = None - self._hvac_mode = None - self._schema_names = None - self._schema_status = None - self._temperature = None - self._setpoint = None - self._water_pressure = None - self._schedule_temp = None - self._hvac_mode = None - self._single_thermostat = self._api.single_master_thermostat() - self._unique_id = f"{dev_id}-climate" - - @property - def hvac_action(self): - """Return the current action.""" - if self._single_thermostat: - if self._heating_state: - return CURRENT_HVAC_HEAT - if self._cooling_state: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - if self._setpoint > self._temperature: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attributes = {} - if self._schema_names: - attributes["available_schemas"] = self._schema_names - if self._selected_schema: - attributes["selected_schema"] = self._selected_schema - return attributes + super().__init__(coordinator, device_id) + self._attr_extra_state_attributes = {} + self._attr_unique_id = f"{device_id}-climate" + self._attr_name = self.device.get("name") + + # Determine preset modes + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + if presets := self.device.get("presets"): + self._attr_supported_features |= SUPPORT_PRESET_MODE + self._attr_preset_modes = list(presets) + + # Determine hvac modes and current hvac mode + self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF] + if self.coordinator.data.gateway.get("cooling_present"): + self._attr_hvac_modes.append(HVAC_MODE_COOL) + if self.device.get("available_schedules") != ["None"]: + self._attr_hvac_modes.append(HVAC_MODE_AUTO) + + self._attr_min_temp = self.device.get("lower_bound", DEFAULT_MIN_TEMP) + self._attr_max_temp = self.device.get("upper_bound", DEFAULT_MAX_TEMP) + if resolution := self.device.get("resolution"): + # Ensure we don't drop below 0.1 + self._attr_target_temperature_step = max(resolution, 0.1) @property - def preset_modes(self): - """Return the available preset modes list.""" - return self._presets_list + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.device["sensors"].get("temperature") @property - def hvac_modes(self): - """Return the available hvac modes list.""" - if self._compressor_state is not None: - return HVAC_MODES_HEAT_COOL - return HVAC_MODES_HEAT_ONLY + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self.device["sensors"].get("setpoint") @property - def hvac_mode(self): - """Return current active hvac state.""" - return self._hvac_mode + def hvac_mode(self) -> str: + """Return HVAC operation ie. heat, cool mode.""" + if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes: + return HVAC_MODE_OFF + return mode @property - def target_temperature(self): - """Return the target_temperature.""" - return self._setpoint - - @property - def preset_mode(self): - """Return the active preset.""" - if self._presets: - return self._preset_mode - return None - - @property - def current_temperature(self): - """Return the current room temperature.""" - return self._temperature - - @property - def min_temp(self): - """Return the minimal temperature possible to set.""" - return self._min_temp + def hvac_action(self) -> str: + """Return the current running hvac operation if supported.""" + # When control_state is present, prefer this data + if "control_state" in self.device: + if self.device.get("control_state") == "cooling": + return CURRENT_HVAC_COOL + # Support preheating state as heating, until preheating is added as a separate state + if self.device.get("control_state") in ["heating", "preheating"]: + return CURRENT_HVAC_HEAT + else: + heater_central_data = self.coordinator.data.devices[ + self.coordinator.data.gateway["heater_id"] + ] + if heater_central_data["binary_sensors"].get("heating_state"): + return CURRENT_HVAC_HEAT + if heater_central_data["binary_sensors"].get("cooling_state"): + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE @property - def max_temp(self): - """Return the maximum temperature possible to set.""" - return self._max_temp + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.device.get("active_preset") @property - def temperature_unit(self): - """Return the unit of measured temperature.""" - return TEMP_CELSIUS - - async def async_set_temperature(self, **kwargs): + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + return { + "available_schemas": self.device.get("available_schedules"), + "selected_schema": self.device.get("selected_schedule"), + } + + @plugwise_command + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if (temperature is not None) and ( - self._min_temp < temperature < self._max_temp + if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or ( + self._attr_max_temp < temperature < self._attr_min_temp ): - try: - await self._api.set_temperature(self._loc_id, temperature) - self._setpoint = temperature - self.async_write_ha_state() - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - else: - _LOGGER.error("Invalid temperature requested") + raise ValueError("Invalid temperature requested") + await self.coordinator.api.set_temperature(self.device["location"], temperature) - async def async_set_hvac_mode(self, hvac_mode): + @plugwise_command + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set the hvac mode.""" - state = SCHEDULE_OFF - if hvac_mode == HVAC_MODE_AUTO: - state = SCHEDULE_ON - try: - await self._api.set_temperature(self._loc_id, self._schedule_temp) - self._setpoint = self._schedule_temp - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - try: - await self._api.set_schedule_state( - self._loc_id, self._last_active_schema, state - ) - self._hvac_mode = hvac_mode - self.async_write_ha_state() - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - - async def async_set_preset_mode(self, preset_mode): - """Set the preset mode.""" - try: - await self._api.set_preset(self._loc_id, preset_mode) - self._preset_mode = preset_mode - self._setpoint = self._presets.get(self._preset_mode, "none")[0] - self.async_write_ha_state() - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - - @callback - def _async_process_data(self): - """Update the data for this climate device.""" - climate_data = self._api.get_device_data(self._dev_id) - heater_central_data = self._api.get_device_data(self._api.heater_id) + if hvac_mode == HVAC_MODE_AUTO and not self.device.get("schedule_temperature"): + raise ValueError("Cannot set HVAC mode to Auto: No schedule available") - if "setpoint" in climate_data: - self._setpoint = climate_data["setpoint"] - if "temperature" in climate_data: - self._temperature = climate_data["temperature"] - if "schedule_temperature" in climate_data: - self._schedule_temp = climate_data["schedule_temperature"] - if "available_schedules" in climate_data: - self._schema_names = climate_data["available_schedules"] - if "selected_schedule" in climate_data: - self._selected_schema = climate_data["selected_schedule"] - self._schema_status = False - if self._selected_schema is not None: - self._schema_status = True - if "last_used" in climate_data: - self._last_active_schema = climate_data["last_used"] - if "presets" in climate_data: - self._presets = climate_data["presets"] - if self._presets: - self._presets_list = list(self._presets) - if "active_preset" in climate_data: - self._preset_mode = climate_data["active_preset"] - - if heater_central_data.get("heating_state") is not None: - self._heating_state = heater_central_data["heating_state"] - if heater_central_data.get("cooling_state") is not None: - self._cooling_state = heater_central_data["cooling_state"] - if heater_central_data.get("compressor_state") is not None: - self._compressor_state = heater_central_data["compressor_state"] - - self._hvac_mode = HVAC_MODE_HEAT - if self._compressor_state is not None: - self._hvac_mode = HVAC_MODE_HEAT_COOL - - if self._schema_status: - self._hvac_mode = HVAC_MODE_AUTO + await self.coordinator.api.set_schedule_state( + self.device["location"], + self.device.get("last_used"), + "on" if hvac_mode == HVAC_MODE_AUTO else "off", + ) - self.async_write_ha_state() + @plugwise_command + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.coordinator.api.set_preset(self.device["location"], preset_mode) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index a120daf0083be..e69d92e3cd0ca 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -1,38 +1,34 @@ """Config flow for Plugwise integration.""" from __future__ import annotations -import logging +from typing import Any from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.components import zeroconf +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_BASE, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( API, DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, - FLOW_NET, FLOW_SMILE, FLOW_STRETCH, - FLOW_TYPE, - FLOW_USB, + LOGGER, PW_TYPE, SMILE, STRETCH, @@ -40,16 +36,6 @@ ZEROCONF_MAP, ) -_LOGGER = logging.getLogger(__name__) - -CONF_MANUAL_PATH = "Enter Manually" - -CONNECTION_SCHEMA = vol.Schema( - {vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])} -) - -# PLACEHOLDER USB connection validation - def _base_gw_schema(discovery_info): """Generate base schema for gateways.""" @@ -67,14 +53,13 @@ def _base_gw_schema(discovery_info): return vol.Schema(base_gw_schema) -async def validate_gw_input(hass: core.HomeAssistant, data): +async def validate_gw_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile: """ Validate whether the user input allows us to connect to the gateway. Data has the keys from _base_gw_schema() with values provided by the user. """ websession = async_get_clientsession(hass, verify_ssl=False) - api = Smile( host=data[CONF_HOST], password=data[CONF_PASSWORD], @@ -83,35 +68,25 @@ async def validate_gw_input(hass: core.HomeAssistant, data): timeout=30, websession=websession, ) - - try: - await api.connect() - except InvalidAuthentication as err: - raise InvalidAuth from err - except PlugwiseException as err: - raise CannotConnect from err - + await api.connect() return api -class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" VERSION = 1 - def __init__(self): - """Initialize the Plugwise config flow.""" - self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None - self._username: str = DEFAULT_USERNAME + discovery_info: ZeroconfServiceInfo | None = None + _username: str = DEFAULT_USERNAME async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info _properties = discovery_info.properties - # unique_id is needed here, to be able to determine whether the discovered device is known, or not. unique_id = discovery_info.hostname.split(".")[0] await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: discovery_info.host}) @@ -122,24 +97,26 @@ async def async_step_zeroconf( _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" - self.context["title_placeholders"] = { - CONF_HOST: discovery_info.host, - CONF_NAME: _name, - CONF_PORT: discovery_info.port, - CONF_USERNAME: self._username, - } - return await self.async_step_user_gateway() - - # PLACEHOLDER USB step_user + self.context.update( + { + "title_placeholders": { + CONF_HOST: discovery_info.host, + CONF_NAME: _name, + CONF_PORT: discovery_info.port, + CONF_USERNAME: self._username, + }, + "configuration_url": f"http://{discovery_info.host}:{discovery_info.port}", + } + ) + return await self.async_step_user() - async def async_step_user_gateway(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step when using network/gateway setups.""" - api = None errors = {} if user_input is not None: - user_input.pop(FLOW_TYPE, None) - if self.discovery_info: user_input[CONF_HOST] = self.discovery_info.host user_input[CONF_PORT] = self.discovery_info.port @@ -147,16 +124,14 @@ async def async_step_user_gateway(self, user_input=None): try: api = await validate_gw_input(self.hass, user_input) - - except CannotConnect: - errors[CONF_BASE] = "cannot_connect" - except InvalidAuth: + except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" + except PlugwiseException: + errors[CONF_BASE] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" - - if not errors: + else: await self.async_set_unique_id( api.smile_hostname or api.gateway_id, raise_on_progress=False ) @@ -165,62 +140,8 @@ async def async_step_user_gateway(self, user_input=None): user_input[PW_TYPE] = API return self.async_create_entry(title=api.smile_name, data=user_input) - return self.async_show_form( - step_id="user_gateway", - data_schema=_base_gw_schema(self.discovery_info), - errors=errors, - ) - - async def async_step_user(self, user_input=None): - """Handle the initial step when using network/gateway setups.""" - errors = {} - if user_input is not None: - if user_input[FLOW_TYPE] == FLOW_NET: - return await self.async_step_user_gateway() - - # PLACEHOLDER for USB_FLOW - return self.async_show_form( step_id="user", - data_schema=CONNECTION_SCHEMA, + data_schema=_base_gw_schema(self.discovery_info), errors=errors, ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return PlugwiseOptionsFlowHandler(config_entry) - - -class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): - """Plugwise option flow.""" - - def __init__(self, config_entry): - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Manage the Plugwise options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - api = self.hass.data[DOMAIN][self.config_entry.entry_id][API] - interval = DEFAULT_SCAN_INTERVAL[api.smile_type] - - data = { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get(CONF_SCAN_INTERVAL, interval), - ): int - } - - return self.async_show_form(step_id="init", data_schema=vol.Schema(data)) - - -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/plugwise/const.py b/homeassistant/components/plugwise/const.py index 9c6823e22e475..adcd68ed50e8b 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -1,24 +1,22 @@ """Constants for Plugwise component.""" +from datetime import timedelta +import logging + from homeassistant.const import Platform -API = "api" -ATTR_ILLUMINANCE = "illuminance" -COORDINATOR = "coordinator" -DEVICE_STATE = "device_state" DOMAIN = "plugwise" -FLOW_NET = "Network: Smile/Stretch" + +LOGGER = logging.getLogger(__package__) + +API = "api" FLOW_SMILE = "smile (Adam/Anna/P1)" FLOW_STRETCH = "stretch (Stretch)" FLOW_TYPE = "flow_type" -FLOW_USB = "USB: Stick - Coming soon" GATEWAY = "gateway" PW_TYPE = "plugwise_type" -SCHEDULE_OFF = "false" -SCHEDULE_ON = "true" SMILE = "smile" STRETCH = "stretch" STRETCH_USERNAME = "stretch" -UNDO_UPDATE_LISTENER = "undo_update_listener" UNIT_LUMEN = "lm" PLATFORMS_GATEWAY = [ @@ -27,7 +25,6 @@ Platform.SENSOR, Platform.SWITCH, ] -SENSOR_PLATFORMS = [Platform.SENSOR, Platform.SWITCH] ZEROCONF_MAP = { "smile": "P1", "smile_thermo": "Anna", @@ -35,38 +32,14 @@ "stretch": "Stretch", } -# Sensor mapping -SENSOR_MAP_DEVICE_CLASS = 2 -SENSOR_MAP_MODEL = 0 -SENSOR_MAP_STATE_CLASS = 3 -SENSOR_MAP_UOM = 1 # Default directives DEFAULT_MAX_TEMP = 30 DEFAULT_MIN_TEMP = 4 -DEFAULT_NAME = "Smile" DEFAULT_PORT = 80 DEFAULT_SCAN_INTERVAL = { - "power": 10, - "stretch": 60, - "thermostat": 60, + "power": timedelta(seconds=10), + "stretch": timedelta(seconds=60), + "thermostat": timedelta(seconds=60), } -DEFAULT_TIMEOUT = 60 DEFAULT_USERNAME = "smile" - -# Configuration directives -CONF_GAS = "gas" -CONF_MAX_TEMP = "max_temp" -CONF_MIN_TEMP = "min_temp" -CONF_POWER = "power" -CONF_THERMOSTAT = "thermostat" - -# Icons -COOL_ICON = "mdi:snowflake" -FLAME_ICON = "mdi:fire" -FLOW_OFF_ICON = "mdi:water-pump-off" -FLOW_ON_ICON = "mdi:water-pump" -IDLE_ICON = "mdi:circle-off-outline" -SWITCH_ICON = "mdi:electric-switch" -NO_NOTIFICATION_ICON = "mdi:mailbox-outline" -NOTIFICATION_ICON = "mdi:mailbox-up-outline" diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py new file mode 100644 index 0000000000000..1c8de0c654434 --- /dev/null +++ b/homeassistant/components/plugwise/coordinator.py @@ -0,0 +1,55 @@ +"""DataUpdateCoordinator for Plugwise.""" +from datetime import timedelta +from typing import Any, NamedTuple + +from plugwise import Smile +from plugwise.exceptions import PlugwiseException, XMLDataMissingError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER + + +class PlugwiseData(NamedTuple): + """Plugwise data stored in the DataUpdateCoordinator.""" + + gateway: dict[str, Any] + devices: dict[str, dict[str, Any]] + + +class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]): + """Class to manage fetching Plugwise data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, api: Smile) -> 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) + ), + # Don't refresh immediately, give the device time to process + # the change in state before we query it. + request_refresh_debouncer=Debouncer( + hass, + LOGGER, + cooldown=1.5, + immediate=False, + ), + ) + self.api = api + + async def _async_update_data(self) -> PlugwiseData: + """Fetch data from Plugwise.""" + try: + data = await self.api.async_update() + except XMLDataMissingError as err: + raise UpdateFailed( + f"No XML data received for: {self.api.smile_name}" + ) from err + except PlugwiseException as err: + raise UpdateFailed(f"Updated failed for: {self.api.smile_name}") from err + return PlugwiseData(*data) diff --git a/homeassistant/components/plugwise/diagnostics.py b/homeassistant/components/plugwise/diagnostics.py new file mode 100644 index 0000000000000..ef54efbe96d98 --- /dev/null +++ b/homeassistant/components/plugwise/diagnostics.py @@ -0,0 +1,21 @@ +"""Diagnostics support for Plugwise.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + return { + "gateway": coordinator.data.gateway, + "devices": coordinator.data.devices, + } diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py new file mode 100644 index 0000000000000..b172b5468b05a --- /dev/null +++ b/homeassistant/components/plugwise/entity.py @@ -0,0 +1,78 @@ +"""Generic Plugwise Entity Class.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + CONNECTION_ZIGBEE, +) +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import PlugwiseData, PlugwiseDataUpdateCoordinator + + +class PlugwiseEntity(CoordinatorEntity[PlugwiseData]): + """Represent a PlugWise Entity.""" + + coordinator: PlugwiseDataUpdateCoordinator + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialise the gateway.""" + super().__init__(coordinator) + self._dev_id = device_id + + configuration_url: str | None = None + if entry := self.coordinator.config_entry: + configuration_url = f"http://{entry.data[CONF_HOST]}" + + data = coordinator.data.devices[device_id] + connections = set() + if mac := data.get("mac_address"): + connections.add((CONNECTION_NETWORK_MAC, mac)) + if mac := data.get("zigbee_mac_address"): + connections.add((CONNECTION_ZIGBEE, mac)) + + self._attr_device_info = DeviceInfo( + configuration_url=configuration_url, + identifiers={(DOMAIN, device_id)}, + connections=connections, + manufacturer=data.get("vendor"), + model=data.get("model"), + name=f"Smile {coordinator.data.gateway['smile_name']}", + sw_version=data.get("fw"), + hw_version=data.get("hw"), + ) + + if device_id != coordinator.data.gateway["gateway_id"]: + self._attr_device_info.update( + { + ATTR_NAME: data.get("name"), + ATTR_VIA_DEVICE: ( + DOMAIN, + str(self.coordinator.data.gateway["gateway_id"]), + ), + } + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self._dev_id in self.coordinator.data.devices + + @property + def device(self) -> dict[str, Any]: + """Return data for this device.""" + return self.coordinator.data.devices[self._dev_id] + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + self._handle_coordinator_update() + await super().async_added_to_hass() diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index af4472ac7ae76..71ca2af953735 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -2,61 +2,29 @@ from __future__ import annotations import asyncio -from datetime import timedelta -import logging - -import async_timeout -from plugwise.exceptions import ( - InvalidAuthentication, - PlugwiseException, - XMLDataMissingError, -) +from typing import Any + +from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_CONFIGURATION_URL, - ATTR_MODEL, - ATTR_VIA_DEVICE, - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) +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 from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - API, - COORDINATOR, - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, - DEFAULT_TIMEOUT, - DEFAULT_USERNAME, - DOMAIN, - GATEWAY, - PLATFORMS_GATEWAY, - PW_TYPE, - SENSOR_PLATFORMS, - UNDO_UPDATE_LISTENER, -) - -_LOGGER = logging.getLogger(__name__) +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries + +from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, LOGGER, PLATFORMS_GATEWAY +from .coordinator import PlugwiseDataUpdateCoordinator async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" - websession = async_get_clientsession(hass, verify_ssl=False) + await 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), @@ -68,164 +36,62 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: connected = await api.connect() - - if not connected: - _LOGGER.error("Unable to connect to Smile") - raise ConfigEntryNotReady - except InvalidAuthentication: - _LOGGER.error("Invalid username or Smile ID") + LOGGER.error("Invalid username or Smile ID") return False - except PlugwiseException as err: - _LOGGER.error("Error while communicating to device %s", api.smile_name) - raise ConfigEntryNotReady from err - + raise ConfigEntryNotReady( + f"Error while communicating to device {api.smile_name}" + ) from err except asyncio.TimeoutError as err: - _LOGGER.error("Timeout while connecting to Smile %s", api.smile_name) - raise ConfigEntryNotReady from err - - update_interval = timedelta( - seconds=entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL[api.smile_type] - ) - ) - - async def async_update_data(): - """Update data via API endpoint.""" - try: - async with async_timeout.timeout(DEFAULT_TIMEOUT): - await api.full_update_device() - return True - except XMLDataMissingError as err: - raise UpdateFailed("Smile update failed") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"Smile {api.smile_name}", - update_method=async_update_data, - update_interval=update_interval, - ) - - await coordinator.async_config_entry_first_refresh() + 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) - undo_listener = entry.add_update_listener(_update_listener) + coordinator = PlugwiseDataUpdateCoordinator(hass, api) + await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "api": api, - COORDINATOR: coordinator, - PW_TYPE: GATEWAY, - UNDO_UPDATE_LISTENER: undo_listener, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers={(DOMAIN, api.gateway_id)}, + identifiers={(DOMAIN, str(api.gateway_id))}, manufacturer="Plugwise", name=entry.title, model=f"Smile {api.smile_name}", sw_version=api.smile_version[0], ) - single_master_thermostat = api.single_master_thermostat() - - platforms = PLATFORMS_GATEWAY - if single_master_thermostat is None: - platforms = SENSOR_PLATFORMS - - hass.config_entries.async_setup_platforms(entry, platforms) + hass.config_entries.async_setup_platforms(entry, PLATFORMS_GATEWAY) return True -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Handle options update.""" - coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] - update_interval = entry.options.get(CONF_SCAN_INTERVAL) - if update_interval is None: - api = hass.data[DOMAIN][entry.entry_id][API] - update_interval = DEFAULT_SCAN_INTERVAL[api.smile_type] - - coordinator.update_interval = timedelta(seconds=update_interval) - - async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( entry, PLATFORMS_GATEWAY - ) - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - - if unload_ok: + ): hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok -class SmileGateway(CoordinatorEntity): - """Represent Smile Gateway.""" - - def __init__(self, api, coordinator, name, dev_id): - """Initialise the gateway.""" - super().__init__(coordinator) - - self._api = api - self._name = name - self._dev_id = dev_id - - self._unique_id = None - self._model = None - - self._entity_name = self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the entity, if any.""" - return self._name - - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - device_information = DeviceInfo( - identifiers={(DOMAIN, self._dev_id)}, - name=self._entity_name, - manufacturer="Plugwise", - ) - - if entry := self.coordinator.config_entry: - device_information[ - ATTR_CONFIGURATION_URL - ] = f"http://{entry.data[CONF_HOST]}" - - if self._model is not None: - device_information[ATTR_MODEL] = self._model.replace("_", " ").title() - - if self._dev_id != self._api.gateway_id: - device_information[ATTR_VIA_DEVICE] = (DOMAIN, self._api.gateway_id) - - return device_information +@callback +def async_migrate_entity_entry(entry: RegistryEntry) -> dict[str, Any] | None: + """Migrate Plugwise entity entries. - async def async_added_to_hass(self): - """Subscribe to updates.""" - self._async_process_data() - self.async_on_remove( - self.coordinator.async_add_listener(self._async_process_data) - ) + - Migrates unique ID from old relay switches to the new unique ID + """ + if entry.domain == SWITCH_DOMAIN and entry.unique_id.endswith("-plug"): + return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")} - @callback - def _async_process_data(self): - """Interpret and process API data.""" - raise NotImplementedError + # No migration needed + return None diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f81c240284621..4f1417ae01806 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,9 +2,10 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.8.5"], - "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], + "requirements": ["plugwise==0.16.6"], + "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["crcmod", "plugwise"] } diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 9307cb3f98fa8..4ee75e21a45a0 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -1,9 +1,10 @@ """Plugwise Sensor component for Home Assistant.""" -import logging +from __future__ import annotations from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -16,213 +17,246 @@ TEMP_CELSIUS, VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - COOL_ICON, - COORDINATOR, - DEVICE_STATE, - DOMAIN, - FLAME_ICON, - IDLE_ICON, - SENSOR_MAP_DEVICE_CLASS, - SENSOR_MAP_MODEL, - SENSOR_MAP_STATE_CLASS, - SENSOR_MAP_UOM, - UNIT_LUMEN, +from .const import DOMAIN, UNIT_LUMEN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="setpoint", + name="Setpoint", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="intended_boiler_temperature", + name="Intended Boiler Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="temperature_difference", + name="Temperature Difference", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="outdoor_temperature", + name="Outdoor Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="water_temperature", + name="Water Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="return_temperature", + name="Return Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_consumed", + name="Electricity Consumed", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_produced", + name="Electricity Produced", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_consumed_interval", + name="Electricity Consumed Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_consumed_peak_interval", + name="Electricity Consumed Peak Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_consumed_off_peak_interval", + name="Electricity Consumed Off Peak Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_produced_interval", + name="Electricity Produced Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_produced_peak_interval", + name="Electricity Produced Peak Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_produced_off_peak_interval", + name="Electricity Produced Off Peak Interval", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="electricity_consumed_off_peak_point", + name="Electricity Consumed Off Peak Point", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_consumed_peak_point", + name="Electricity Consumed Peak Point", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_consumed_off_peak_cumulative", + name="Electricity Consumed Off Peak Cumulative", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="electricity_consumed_peak_cumulative", + name="Electricity Consumed Peak Cumulative", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="electricity_produced_off_peak_point", + name="Electricity Produced Off Peak Point", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_produced_peak_point", + name="Electricity Produced Peak Point", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_produced_off_peak_cumulative", + name="Electricity Produced Off Peak Cumulative", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="electricity_produced_peak_cumulative", + name="Electricity Produced Peak Cumulative", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="gas_consumed_interval", + name="Gas Consumed Interval", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="gas_consumed_cumulative", + name="Gas Consumed Cumulative", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="net_electricity_point", + name="Net Electricity Point", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="net_electricity_cumulative", + name="Net Electricity Cumulative", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="illuminance", + name="Illuminance", + native_unit_of_measurement=UNIT_LUMEN, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="modulation_level", + name="Modulation Level", + icon="mdi:percent", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="valve_position", + name="Valve Position", + icon="mdi:valve", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="water_pressure", + name="Water Pressure", + native_unit_of_measurement=PRESSURE_BAR, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="humidity", + name="Relative Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), ) -from .gateway import SmileGateway - -_LOGGER = logging.getLogger(__name__) - -ATTR_TEMPERATURE = [ - "Temperature", - TEMP_CELSIUS, - SensorDeviceClass.TEMPERATURE, - SensorStateClass.MEASUREMENT, -] -ATTR_BATTERY_LEVEL = [ - "Charge", - PERCENTAGE, - SensorDeviceClass.BATTERY, - SensorStateClass.MEASUREMENT, -] -ATTR_ILLUMINANCE = [ - "Illuminance", - UNIT_LUMEN, - SensorDeviceClass.ILLUMINANCE, - SensorStateClass.MEASUREMENT, -] -ATTR_PRESSURE = [ - "Pressure", - PRESSURE_BAR, - SensorDeviceClass.PRESSURE, - SensorStateClass.MEASUREMENT, -] - -TEMP_SENSOR_MAP: dict[str, list] = { - "setpoint": ATTR_TEMPERATURE, - "temperature": ATTR_TEMPERATURE, - "intended_boiler_temperature": ATTR_TEMPERATURE, - "temperature_difference": ATTR_TEMPERATURE, - "outdoor_temperature": ATTR_TEMPERATURE, - "water_temperature": ATTR_TEMPERATURE, - "return_temperature": ATTR_TEMPERATURE, -} - -ENERGY_SENSOR_MAP: dict[str, list] = { - "electricity_consumed": [ - "Current Consumed Power", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_produced": [ - "Current Produced Power", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_consumed_interval": [ - "Consumed Power Interval", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_consumed_peak_interval": [ - "Consumed Power Interval", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_consumed_off_peak_interval": [ - "Consumed Power Interval (off peak)", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_produced_interval": [ - "Produced Power Interval", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_produced_peak_interval": [ - "Produced Power Interval", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_produced_off_peak_interval": [ - "Produced Power Interval (off peak)", - ENERGY_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], - "electricity_consumed_off_peak_point": [ - "Current Consumed Power (off peak)", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_consumed_peak_point": [ - "Current Consumed Power", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_consumed_off_peak_cumulative": [ - "Cumulative Consumed Power (off peak)", - ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - ], - "electricity_consumed_peak_cumulative": [ - "Cumulative Consumed Power", - ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - ], - "electricity_produced_off_peak_point": [ - "Current Produced Power (off peak)", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_produced_peak_point": [ - "Current Produced Power", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "electricity_produced_off_peak_cumulative": [ - "Cumulative Produced Power (off peak)", - ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - ], - "electricity_produced_peak_cumulative": [ - "Cumulative Produced Power", - ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - ], - "gas_consumed_interval": [ - "Current Consumed Gas Interval", - VOLUME_CUBIC_METERS, - SensorDeviceClass.GAS, - SensorStateClass.TOTAL, - ], - "gas_consumed_cumulative": [ - "Consumed Gas", - VOLUME_CUBIC_METERS, - SensorDeviceClass.GAS, - SensorStateClass.TOTAL_INCREASING, - ], - "net_electricity_point": [ - "Current net Power", - POWER_WATT, - SensorDeviceClass.POWER, - SensorStateClass.MEASUREMENT, - ], - "net_electricity_cumulative": [ - "Cumulative net Power", - ENERGY_KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL, - ], -} - -MISC_SENSOR_MAP: dict[str, list] = { - "battery": ATTR_BATTERY_LEVEL, - "illuminance": ATTR_ILLUMINANCE, - "modulation_level": [ - "Heater Modulation Level", - PERCENTAGE, - None, - SensorStateClass.MEASUREMENT, - ], - "valve_position": [ - "Valve Position", - PERCENTAGE, - None, - SensorStateClass.MEASUREMENT, - ], - "water_pressure": ATTR_PRESSURE, -} - -INDICATE_ACTIVE_LOCAL_DEVICE = [ - "cooling_state", - "flame_state", -] - -CUSTOM_ICONS = { - "gas_consumed_interval": "mdi:fire", - "gas_consumed_cumulative": "mdi:fire", - "modulation_level": "mdi:percent", - "valve_position": "mdi:valve", -} async def async_setup_entry( @@ -231,214 +265,44 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Smile sensors from a config entry.""" - api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] - - entities: list[SmileSensor] = [] - all_devices = api.get_all_devices() - single_thermostat = api.single_master_thermostat() - for dev_id, device_properties in all_devices.items(): - data = api.get_device_data(dev_id) - for sensor, sensor_type in { - **TEMP_SENSOR_MAP, - **ENERGY_SENSOR_MAP, - **MISC_SENSOR_MAP, - }.items(): - if data.get(sensor) is None: + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[PlugwiseSensorEnity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in SENSORS: + if ( + "sensors" not in device + or device["sensors"].get(description.key) is None + ): continue - if "power" in device_properties["types"]: - model = None - - if "plug" in device_properties["types"]: - model = "Metered Switch" - - entities.append( - PwPowerSensor( - api, - coordinator, - device_properties["name"], - dev_id, - sensor, - sensor_type, - model, - ) - ) - else: - entities.append( - PwThermostatSensor( - api, - coordinator, - device_properties["name"], - dev_id, - sensor, - sensor_type, - ) - ) - - if single_thermostat is False: - for state in INDICATE_ACTIVE_LOCAL_DEVICE: - if state not in data: - continue - - entities.append( - PwAuxDeviceSensor( - api, - coordinator, - device_properties["name"], - dev_id, - DEVICE_STATE, - ) + entities.append( + PlugwiseSensorEnity( + coordinator, + device_id, + description, ) - break + ) - async_add_entities(entities, True) + async_add_entities(entities) -class SmileSensor(SmileGateway, SensorEntity): - """Represent Smile Sensors.""" +class PlugwiseSensorEnity(PlugwiseEntity, SensorEntity): + """Represent Plugwise Sensors.""" - def __init__(self, api, coordinator, name, dev_id, sensor): + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: SensorEntityDescription, + ) -> None: """Initialise the sensor.""" - super().__init__(api, coordinator, name, dev_id) - - self._sensor = sensor - - self._dev_class = None - self._icon = None - self._state = None - self._state_class = None - self._unit_of_measurement = None - - if dev_id == self._api.heater_id: - self._entity_name = "Auxiliary" - - sensorname = sensor.replace("_", " ").title() - self._name = f"{self._entity_name} {sensorname}" - - if dev_id == self._api.gateway_id: - self._entity_name = f"Smile {self._entity_name}" - - self._unique_id = f"{dev_id}-{sensor}" - - @property - def device_class(self): - """Device class of this entity.""" - return self._dev_class + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property - def icon(self): - """Return the icon of this entity.""" - return self._icon - - @property - def native_value(self): - """Return the state of this entity.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def state_class(self): - """Return the state_class of this entity.""" - return self._state_class - - -class PwThermostatSensor(SmileSensor): - """Thermostat (or generic) sensor devices.""" - - def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type): - """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id, sensor) - - self._icon = None - self._model = sensor_type[SENSOR_MAP_MODEL] - self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] - self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] - self._state_class = sensor_type[SENSOR_MAP_STATE_CLASS] - - @callback - def _async_process_data(self): - """Update the entity.""" - if not (data := self._api.get_device_data(self._dev_id)): - _LOGGER.error("Received no data for device %s", self._entity_name) - self.async_write_ha_state() - return - - if data.get(self._sensor) is not None: - self._state = data[self._sensor] - self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) - - self.async_write_ha_state() - - -class PwAuxDeviceSensor(SmileSensor): - """Auxiliary Device Sensors.""" - - def __init__(self, api, coordinator, name, dev_id, sensor): - """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id, sensor) - - self._cooling_state = False - self._heating_state = False - - @callback - def _async_process_data(self): - """Update the entity.""" - if not (data := self._api.get_device_data(self._dev_id)): - _LOGGER.error("Received no data for device %s", self._entity_name) - self.async_write_ha_state() - return - - if data.get("heating_state") is not None: - self._heating_state = data["heating_state"] - if data.get("cooling_state") is not None: - self._cooling_state = data["cooling_state"] - - self._state = "idle" - self._icon = IDLE_ICON - if self._heating_state: - self._state = "heating" - self._icon = FLAME_ICON - if self._cooling_state: - self._state = "cooling" - self._icon = COOL_ICON - - self.async_write_ha_state() - - -class PwPowerSensor(SmileSensor): - """Power sensor entities.""" - - def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type, model): - """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id, sensor) - - self._icon = None - self._model = model - if model is None: - self._model = sensor_type[SENSOR_MAP_MODEL] - - self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] - self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] - self._state_class = sensor_type[SENSOR_MAP_STATE_CLASS] - - if dev_id == self._api.gateway_id: - self._model = "P1 DSMR" - - @callback - def _async_process_data(self): - """Update the entity.""" - if not (data := self._api.get_device_data(self._dev_id)): - _LOGGER.error("Received no data for device %s", self._entity_name) - self.async_write_ha_state() - return - - if data.get(self._sensor) is not None: - self._state = data[self._sensor] - self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) - - self.async_write_ha_state() + def native_value(self) -> int | float | None: + """Return the value reported by the sensor.""" + return self.device["sensors"].get(self.entity_description.key) diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 1aa8bdc51d739..45a10297ed557 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -1,17 +1,42 @@ """Plugwise Switch component for HomeAssistant.""" -import logging +from __future__ import annotations -from plugwise.exceptions import PlugwiseException +from typing import Any -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import COORDINATOR, DOMAIN, SWITCH_ICON -from .gateway import SmileGateway - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity +from .util import plugwise_command + +SWITCHES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="dhw_cm_switch", + name="DHW Comfort Mode", + icon="mdi:water-plus", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key="lock", + name="Lock", + icon="mdi:lock", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key="relay", + name="Relay", + device_class=SwitchDeviceClass.SWITCH, + ), +) async def async_setup_entry( @@ -20,100 +45,52 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Smile switches from a config entry.""" - # PLACEHOLDER USB entry setup - return await async_setup_entry_gateway(hass, config_entry, async_add_entities) - - -async def async_setup_entry_gateway(hass, config_entry, async_add_entities): - """Set up the Smile switches from a config entry.""" - api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] - - entities = [] - switch_classes = ["plug", "switch_group"] - - all_devices = api.get_all_devices() - for dev_id, device_properties in all_devices.items(): - members = None - model = None - - if any( - switch_class in device_properties["types"] - for switch_class in switch_classes - ): - if "plug" in device_properties["types"]: - model = "Metered Switch" - if "switch_group" in device_properties["types"]: - members = device_properties["members"] - model = "Switch Group" - - entities.append( - GwSwitch( - api, coordinator, device_properties["name"], dev_id, members, model - ) - ) - - async_add_entities(entities, True) + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities: list[PlugwiseSwitchEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in SWITCHES: + if "switches" not in device or description.key not in device["switches"]: + continue + entities.append(PlugwiseSwitchEntity(coordinator, device_id, description)) + async_add_entities(entities) -class GwSwitch(SmileGateway, SwitchEntity): +class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): """Representation of a Plugwise plug.""" - def __init__(self, api, coordinator, name, dev_id, members, model): + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: SwitchEntityDescription, + ) -> None: """Set up the Plugwise API.""" - super().__init__(api, coordinator, name, dev_id) - - self._members = members - self._model = model - - self._is_on = False - self._icon = SWITCH_ICON - - self._unique_id = f"{dev_id}-plug" - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property - def icon(self): - """Return the icon of this entity.""" - return self._icon + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return self.device["switches"].get(self.entity_description.key) - async def async_turn_on(self, **kwargs): + @plugwise_command + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - try: - state_on = await self._api.set_relay_state( - self._dev_id, self._members, "on" - ) - if state_on: - self._is_on = True - self.async_write_ha_state() - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - - async def async_turn_off(self, **kwargs): + await self.coordinator.api.set_switch_state( + self._dev_id, + self.device.get("members"), + self.entity_description.key, + "on", + ) + + @plugwise_command + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - try: - state_off = await self._api.set_relay_state( - self._dev_id, self._members, "off" - ) - if state_off: - self._is_on = False - self.async_write_ha_state() - except PlugwiseException: - _LOGGER.error("Error while communicating to device") - - @callback - def _async_process_data(self): - """Update the data from the Plugs.""" - if not (data := self._api.get_device_data(self._dev_id)): - _LOGGER.error("Received no data for device %s", self._name) - self.async_write_ha_state() - return - - if "relay" in data: - self._is_on = data["relay"] - - self.async_write_ha_state() + await self.coordinator.api.set_switch_state( + self._dev_id, + self.device.get("members"), + self.entity_description.key, + "off", + ) diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index b81e93e09fd61..a891a74d3ad42 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -1,9 +1,31 @@ { "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_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" + }, + "flow_title": "{name}", "step": { "user": { + "data": { + "flow_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "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" } } }, diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index a375e2c1f67d8..f53f6c7c3794f 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -1,15 +1,41 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", "step": { "user": { "data": { "flow_type": "Tipo de conex\u00e3o" - } + }, + "description": "Produto:", + "title": "Tipo Plugwise" }, "user_gateway": { "data": { - "host": "Endere\u00e7o IP" - } + "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/sk.json b/homeassistant/components/plugwise/translations/sk.json new file mode 100644 index 0000000000000..7124f1e5e28d9 --- /dev/null +++ b/homeassistant/components/plugwise/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user_gateway": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/util.py b/homeassistant/components/plugwise/util.py new file mode 100644 index 0000000000000..58c7715815ea4 --- /dev/null +++ b/homeassistant/components/plugwise/util.py @@ -0,0 +1,36 @@ +"""Utilities for Plugwise.""" +from collections.abc import Awaitable, Callable, Coroutine +from typing import Any, TypeVar + +from plugwise.exceptions import PlugwiseException +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.exceptions import HomeAssistantError + +from .entity import PlugwiseEntity + +_P = ParamSpec("_P") +_R = TypeVar("_R") +_T = TypeVar("_T", bound=PlugwiseEntity) + + +def plugwise_command( + func: Callable[Concatenate[_T, _P], Awaitable[_R]] # type: ignore[misc] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]: # type: ignore[misc] + """Decorate Plugwise calls that send commands/make changes to the device. + + A decorator that wraps the passed in function, catches Plugwise errors, + and requests an coordinator update to update status of the devices asap. + """ + + async def handler(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R: + try: + return await func(self, *args, **kwargs) + except PlugwiseException as error: + raise HomeAssistantError( + f"Error communicating with API: {error}" + ) from error + finally: + await self.coordinator.async_request_refresh() + + return handler diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index f2cc88538f96d..b2afb55fc5d49 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -11,7 +11,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN from .utils import load_plum @@ -60,6 +59,8 @@ async def async_step_user( title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} ) - async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] | None + ) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 366f770ca3b9e..05eeac20f171a 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -5,5 +5,6 @@ "requirements": ["plumlightpad==0.0.11"], "codeowners": ["@ColinHarrington", "@prystupa"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["plumlightpad"] } diff --git a/homeassistant/components/plum_lightpad/translations/el.json b/homeassistant/components/plum_lightpad/translations/el.json new file mode 100644 index 0000000000000..5f3d7c8b43532 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/el.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/pt-BR.json b/homeassistant/components/plum_lightpad/translations/pt-BR.json new file mode 100644 index 0000000000000..4213b842e462e --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/sk.json b/homeassistant/components/plum_lightpad/translations/sk.json new file mode 100644 index 0000000000000..ee5407aae192a --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index a2070daedd7c1..f74c77ed3a912 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": ["pycketcasts==1.0.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pycketcasts"] } diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 792563d3db856..c74f5745bfcb3 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], "quality_scale": "gold", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pypoint"] } diff --git a/homeassistant/components/point/translations/el.json b/homeassistant/components/point/translations/el.json index fd4186ecf038c..1af8ebfcb4e12 100644 --- a/homeassistant/components/point/translations/el.json +++ b/homeassistant/components/point/translations/el.json @@ -1,10 +1,18 @@ { "config": { "abort": { - "external_setup": "\u03a4\u03bf \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ce\u03c2 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03c1\u03bf\u03ae." + "already_setup": "\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.", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "external_setup": "\u03a4\u03bf \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ce\u03c2 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03c1\u03bf\u03ae.", + "no_flows": "\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.", + "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": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "error": { - "follow_link": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03ba\u03b1\u03b9 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c0\u03c1\u03b9\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae" + "follow_link": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03ba\u03b1\u03b9 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c0\u03c1\u03b9\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae", + "no_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "step": { "auth": { @@ -14,7 +22,9 @@ "user": { "data": { "flow_impl": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2" - } + }, + "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;", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } } diff --git a/homeassistant/components/point/translations/pt-BR.json b/homeassistant/components/point/translations/pt-BR.json index c9384d82c38ae..a940c67daf962 100644 --- a/homeassistant/components/point/translations/pt-BR.json +++ b/homeassistant/components/point/translations/pt-BR.json @@ -1,29 +1,30 @@ { "config": { "abort": { - "already_setup": "Voc\u00ea s\u00f3 pode configurar uma conta Point.", - "authorize_url_timeout": "Excedido tempo limite gerando a URL de autoriza\u00e7\u00e3o.", + "already_setup": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", - "no_flows": "Voc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/)." + "no_flows": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.\nVoc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { - "default": "Autenticado com sucesso com Minut para seu(s) dispositivo(s) Point" + "default": "Autenticado com sucesso" }, "error": { "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", - "no_token": "N\u00e3o autenticado com Minut" + "no_token": "Token de acesso inv\u00e1lido" }, "step": { "auth": { - "description": "Siga o link abaixo e Aceite o acesso \u00e0 sua conta Minut, depois volte e pressione Enviar. \n\n [Link]({authorization_url})", + "description": "Siga o link abaixo e **Aceite** o acesso \u00e0 sua conta Minut, depois volte e pressione **Enviar**. \n\n [Link]({authorization_url})", "title": "Autenticar Ponto" }, "user": { "data": { "flow_impl": "Provedor" }, - "description": "Escolha atrav\u00e9s de qual provedor de autentica\u00e7\u00e3o voc\u00ea deseja autenticar com Point.", - "title": "Provedor de Autentica\u00e7\u00e3o" + "description": "Deseja iniciar a configura\u00e7\u00e3o?", + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } } diff --git a/homeassistant/components/point/translations/sk.json b/homeassistant/components/point/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/point/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json index 798f76e4f6b30..c4d23a6055dfe 100644 --- a/homeassistant/components/point/translations/uk.json +++ b/homeassistant/components/point/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "already_setup": "\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.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 3f9df05d69710..1feba202c0b05 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "already_setup": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json index 697afd541063a..7867df3dbee34 100644 --- a/homeassistant/components/poolsense/manifest.json +++ b/homeassistant/components/poolsense/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/poolsense", "requirements": ["poolsense==0.0.8"], "codeowners": ["@haemishkyd"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["poolsense"] } diff --git a/homeassistant/components/poolsense/translations/el.json b/homeassistant/components/poolsense/translations/el.json new file mode 100644 index 0000000000000..bb70337158e1b --- /dev/null +++ b/homeassistant/components/poolsense/translations/el.json @@ -0,0 +1,20 @@ +{ + "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": { + "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": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "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;", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/pt-BR.json b/homeassistant/components/poolsense/translations/pt-BR.json new file mode 100644 index 0000000000000..8b2e2bddda098 --- /dev/null +++ b/homeassistant/components/poolsense/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "description": "Deseja iniciar a configura\u00e7\u00e3o?", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/sk.json b/homeassistant/components/poolsense/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/poolsense/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index fccd897963167..10504e2aa0662 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,4 +1,7 @@ """The Tesla Powerwall integration.""" +from __future__ import annotations + +import contextlib from datetime import timedelta import logging @@ -8,35 +11,28 @@ APIError, MissingAttributeError, Powerwall, + PowerwallError, PowerwallUnreachableError, ) from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.network import is_ip_address from .const import ( DOMAIN, POWERWALL_API_CHANGED, - POWERWALL_API_CHARGE, - POWERWALL_API_DEVICE_TYPE, - POWERWALL_API_GRID_SERVICES_ACTIVE, - POWERWALL_API_GRID_STATUS, - POWERWALL_API_METERS, - POWERWALL_API_SERIAL_NUMBERS, - POWERWALL_API_SITE_INFO, - POWERWALL_API_SITEMASTER, - POWERWALL_API_STATUS, POWERWALL_COORDINATOR, POWERWALL_HTTP_SESSION, - POWERWALL_OBJECT, + POWERWALL_LOGIN_FAILED_COUNT, UPDATE_INTERVAL, ) +from .models import PowerwallBaseInfo, PowerwallData, PowerwallRuntimeData CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -46,204 +42,194 @@ MAX_LOGIN_FAILURES = 5 +API_CHANGED_ERROR_BODY = ( + "It seems like your powerwall uses an unsupported version. " + "Please update the software of your powerwall or if it is " + "already the newest consider reporting this issue.\nSee logs for more information" +) +API_CHANGED_TITLE = "Unknown powerwall software version" + + +class PowerwallDataManager: + """Class to manager powerwall data and relogin on failure.""" + + def __init__( + self, + hass: HomeAssistant, + power_wall: Powerwall, + ip_address: str, + password: str | None, + runtime_data: PowerwallRuntimeData, + ) -> None: + """Init the data manager.""" + self.hass = hass + self.ip_address = ip_address + self.password = password + self.runtime_data = runtime_data + self.power_wall = power_wall + + @property + def login_failed_count(self) -> int: + """Return the current number of failed logins.""" + return self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] + + @property + def api_changed(self) -> int: + """Return true if the api has changed out from under us.""" + return self.runtime_data[POWERWALL_API_CHANGED] + + def _increment_failed_logins(self) -> None: + self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] += 1 + + def _clear_failed_logins(self) -> None: + self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] = 0 + + def _recreate_powerwall_login(self) -> None: + """Recreate the login on auth failure.""" + http_session = self.runtime_data[POWERWALL_HTTP_SESSION] + http_session.close() + http_session = requests.Session() + self.runtime_data[POWERWALL_HTTP_SESSION] = http_session + self.power_wall = Powerwall(self.ip_address, http_session=http_session) + self.power_wall.login(self.password or "") -async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): - serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] - site_info = powerwall_data[POWERWALL_API_SITE_INFO] - - @callback - def _async_migrator(entity_entry: entity_registry.RegistryEntry): - parts = entity_entry.unique_id.split("_") - # Check if the unique_id starts with the serial_numbers of the powerwalls - if parts[0 : len(serial_numbers)] != serial_numbers: - # The old unique_id ended with the nomianal_system_engery_kWh so we can use that - # to find the old base unique_id and extract the device_suffix. - normalized_energy_index = ( - len(parts) - 1 - parts[::-1].index(str(site_info.nominal_system_energy)) - ) - device_suffix = parts[normalized_energy_index + 1 :] - - new_unique_id = "_".join([*serial_numbers, *device_suffix]) - _LOGGER.info( - "Migrating unique_id from [%s] to [%s]", - entity_entry.unique_id, - new_unique_id, - ) - return {"new_unique_id": new_unique_id} - return None - - await entity_registry.async_migrate_entries(hass, entry_id, _async_migrator) - - -async def _async_handle_api_changed_error( - hass: HomeAssistant, error: MissingAttributeError -): - # The error might include some important information about what exactly changed. - _LOGGER.error(str(error)) - persistent_notification.async_create( - hass, - "It seems like your powerwall uses an unsupported version. " - "Please update the software of your powerwall or if it is " - "already the newest consider reporting this issue.\nSee logs for more information", - title="Unknown powerwall software version", - ) + async def async_update_data(self) -> PowerwallData: + """Fetch data from API endpoint.""" + # Check if we had an error before + _LOGGER.debug("Checking if update failed") + if self.api_changed: + raise UpdateFailed("The powerwall api has changed") + return await self.hass.async_add_executor_job(self._update_data) + + def _update_data(self) -> PowerwallData: + """Fetch data from API endpoint.""" + _LOGGER.debug("Updating data") + for attempt in range(2): + try: + if attempt == 1: + self._recreate_powerwall_login() + data = _fetch_powerwall_data(self.power_wall) + except PowerwallUnreachableError as err: + raise UpdateFailed("Unable to fetch data from powerwall") from err + except MissingAttributeError as err: + _LOGGER.error("The powerwall api has changed: %s", str(err)) + # The error might include some important information about what exactly changed. + persistent_notification.create( + self.hass, API_CHANGED_ERROR_BODY, API_CHANGED_TITLE + ) + self.runtime_data[POWERWALL_API_CHANGED] = True + raise UpdateFailed("The powerwall api has changed") from err + except AccessDeniedError as err: + if attempt == 1: + self._increment_failed_logins() + raise ConfigEntryAuthFailed from err + if self.password is None: + raise ConfigEntryAuthFailed from err + raise UpdateFailed( + f"Login attempt {self.login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}" + ) from err + except APIError as err: + raise UpdateFailed(f"Updated failed due to {err}, will retry") from err + else: + self._clear_failed_logins() + return data + raise RuntimeError("unreachable") async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tesla Powerwall from a config entry.""" - - entry_id = entry.entry_id - - hass.data.setdefault(DOMAIN, {}) http_session = requests.Session() ip_address = entry.data[CONF_IP_ADDRESS] password = entry.data.get(CONF_PASSWORD) power_wall = Powerwall(ip_address, http_session=http_session) try: - powerwall_data = await hass.async_add_executor_job( - _login_and_fetch_base_info, power_wall, password + base_info = await hass.async_add_executor_job( + _login_and_fetch_base_info, power_wall, ip_address, password ) except PowerwallUnreachableError as err: http_session.close() raise ConfigEntryNotReady from err except MissingAttributeError as err: http_session.close() - await _async_handle_api_changed_error(hass, err) + # The error might include some important information about what exactly changed. + _LOGGER.error("The powerwall api has changed: %s", str(err)) + persistent_notification.async_create( + hass, API_CHANGED_ERROR_BODY, API_CHANGED_TITLE + ) return False except AccessDeniedError as err: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() raise ConfigEntryAuthFailed from err - await _migrate_old_unique_ids(hass, entry_id, powerwall_data) - login_failed_count = 0 - - runtime_data = hass.data[DOMAIN][entry.entry_id] = { - POWERWALL_API_CHANGED: False, - POWERWALL_HTTP_SESSION: http_session, - } - - def _recreate_powerwall_login(): - nonlocal http_session - nonlocal power_wall - http_session.close() - http_session = requests.Session() - power_wall = Powerwall(ip_address, http_session=http_session) - runtime_data[POWERWALL_OBJECT] = power_wall - runtime_data[POWERWALL_HTTP_SESSION] = http_session - power_wall.login(password) + gateway_din = base_info.gateway_din + if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id): + hass.config_entries.async_update_entry(entry, unique_id=gateway_din) - async def _async_login_and_retry_update_data(): - """Retry the update after a failed login.""" - nonlocal login_failed_count - # If the session expired, recreate, relogin, and try again - _LOGGER.debug("Retrying login and updating data") - try: - await hass.async_add_executor_job(_recreate_powerwall_login) - data = await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError as err: - login_failed_count += 1 - if login_failed_count == MAX_LOGIN_FAILURES: - raise ConfigEntryAuthFailed from err - raise UpdateFailed( - f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}" - ) from err - except APIError as err: - raise UpdateFailed(f"Updated failed due to {err}, will retry") from err - else: - login_failed_count = 0 - return data - - async def async_update_data(): - """Fetch data from API endpoint.""" - # Check if we had an error before - nonlocal login_failed_count - _LOGGER.debug("Checking if update failed") - if runtime_data[POWERWALL_API_CHANGED]: - return runtime_data[POWERWALL_COORDINATOR].data + runtime_data = PowerwallRuntimeData( + api_changed=False, + base_info=base_info, + http_session=http_session, + login_failed_count=0, + coordinator=None, + ) - _LOGGER.debug("Updating data") - try: - data = await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError as err: - if password is None: - raise ConfigEntryAuthFailed from err - return await _async_login_and_retry_update_data() - except APIError as err: - raise UpdateFailed(f"Updated failed due to {err}, will retry") from err - else: - login_failed_count = 0 - return data + manager = PowerwallDataManager(hass, power_wall, ip_address, password, runtime_data) coordinator = DataUpdateCoordinator( hass, _LOGGER, name="Powerwall site", - update_method=async_update_data, + update_method=manager.async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - runtime_data.update( - { - **powerwall_data, - POWERWALL_OBJECT: power_wall, - POWERWALL_COORDINATOR: coordinator, - } - ) - await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + runtime_data[POWERWALL_COORDINATOR] = coordinator - return True + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data + hass.config_entries.async_setup_platforms(entry, PLATFORMS) -async def _async_update_powerwall_data( - hass: HomeAssistant, entry: ConfigEntry, power_wall: Powerwall -): - """Fetch updated powerwall data.""" - try: - return await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) - except PowerwallUnreachableError as err: - raise UpdateFailed("Unable to fetch data from powerwall") from err - except MissingAttributeError as err: - await _async_handle_api_changed_error(hass, err) - hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True - # Returns the cached data. This data can also be None - return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data + return True -def _login_and_fetch_base_info(power_wall: Powerwall, password: str): +def _login_and_fetch_base_info( + power_wall: Powerwall, host: str, password: str +) -> PowerwallBaseInfo: """Login to the powerwall and fetch the base info.""" if password is not None: power_wall.login(password) - power_wall.detect_and_pin_version() - return call_base_info(power_wall) + return call_base_info(power_wall, host) -def call_base_info(power_wall): - """Wrap powerwall properties to be a callable.""" - serial_numbers = power_wall.get_serial_numbers() +def call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo: + """Return PowerwallBaseInfo for the device.""" # Make sure the serial numbers always have the same order - serial_numbers.sort() - return { - POWERWALL_API_SITE_INFO: power_wall.get_site_info(), - POWERWALL_API_STATUS: power_wall.get_status(), - POWERWALL_API_DEVICE_TYPE: power_wall.get_device_type(), - POWERWALL_API_SERIAL_NUMBERS: serial_numbers, - } + gateway_din = None + with contextlib.suppress(AssertionError, PowerwallError): + gateway_din = power_wall.get_gateway_din().upper() + return PowerwallBaseInfo( + gateway_din=gateway_din, + site_info=power_wall.get_site_info(), + status=power_wall.get_status(), + device_type=power_wall.get_device_type(), + serial_numbers=sorted(power_wall.get_serial_numbers()), + url=f"https://{host}", + ) -def _fetch_powerwall_data(power_wall): +def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData: """Process and update powerwall data.""" - return { - POWERWALL_API_CHARGE: power_wall.get_charge(), - POWERWALL_API_SITEMASTER: power_wall.get_sitemaster(), - POWERWALL_API_METERS: power_wall.get_meters(), - POWERWALL_API_GRID_SERVICES_ACTIVE: power_wall.is_grid_services_active(), - POWERWALL_API_GRID_STATUS: power_wall.get_grid_status(), - } + return PowerwallData( + charge=power_wall.get_charge(), + site_master=power_wall.get_sitemaster(), + meters=power_wall.get_meters(), + grid_services_active=power_wall.is_grid_services_active(), + grid_status=power_wall.get_grid_status(), + ) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 5c8ebbdcf1926..fed47823c7f6a 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -1,4 +1,5 @@ """Support for powerwall binary sensors.""" + from tesla_powerwall import GridStatus, MeterType from homeassistant.components.binary_sensor import ( @@ -9,19 +10,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - DOMAIN, - POWERWALL_API_DEVICE_TYPE, - POWERWALL_API_GRID_SERVICES_ACTIVE, - POWERWALL_API_GRID_STATUS, - POWERWALL_API_METERS, - POWERWALL_API_SERIAL_NUMBERS, - POWERWALL_API_SITE_INFO, - POWERWALL_API_SITEMASTER, - POWERWALL_API_STATUS, - POWERWALL_COORDINATOR, -) +from .const import DOMAIN from .entity import PowerWallEntity +from .models import PowerwallRuntimeData async def async_setup_entry( @@ -29,152 +20,112 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the August sensors.""" - powerwall_data = hass.data[DOMAIN][config_entry.entry_id] - - coordinator = powerwall_data[POWERWALL_COORDINATOR] - site_info = powerwall_data[POWERWALL_API_SITE_INFO] - device_type = powerwall_data[POWERWALL_API_DEVICE_TYPE] - status = powerwall_data[POWERWALL_API_STATUS] - powerwalls_serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] - - entities = [] - for sensor_class in ( - PowerWallRunningSensor, - PowerWallGridServicesActiveSensor, - PowerWallGridStatusSensor, - PowerWallConnectedSensor, - PowerWallChargingStatusSensor, - ): - entities.append( - sensor_class( - coordinator, site_info, status, device_type, powerwalls_serial_numbers + """Set up the powerwall sensors.""" + powerwall_data: PowerwallRuntimeData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + sensor_class(powerwall_data) + for sensor_class in ( + PowerWallRunningSensor, + PowerWallGridServicesActiveSensor, + PowerWallGridStatusSensor, + PowerWallConnectedSensor, + PowerWallChargingStatusSensor, ) - ) - - async_add_entities(entities, True) + ] + ) class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall running sensor.""" - @property - def name(self): - """Device Name.""" - return "Powerwall Status" + _attr_name = "Powerwall Status" + _attr_device_class = BinarySensorDeviceClass.POWER @property - def device_class(self): - """Device Class.""" - return BinarySensorDeviceClass.POWER - - @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_running" @property - def is_on(self): + def is_on(self) -> bool: """Get the powerwall running state.""" - return self.coordinator.data[POWERWALL_API_SITEMASTER].is_running + return self.data.site_master.is_running class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall connected sensor.""" - @property - def name(self): - """Device Name.""" - return "Powerwall Connected to Tesla" + _attr_name = "Powerwall Connected to Tesla" + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY @property - def device_class(self): - """Device Class.""" - return BinarySensorDeviceClass.CONNECTIVITY - - @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_connected_to_tesla" @property - def is_on(self): + def is_on(self) -> bool: """Get the powerwall connected to tesla state.""" - return self.coordinator.data[POWERWALL_API_SITEMASTER].is_connected_to_tesla + return self.data.site_master.is_connected_to_tesla class PowerWallGridServicesActiveSensor(PowerWallEntity, BinarySensorEntity): """Representation of a Powerwall grid services active sensor.""" - @property - def name(self): - """Device Name.""" - return "Grid Services Active" + _attr_name = "Grid Services Active" + _attr_device_class = BinarySensorDeviceClass.POWER @property - def device_class(self): - """Device Class.""" - return BinarySensorDeviceClass.POWER - - @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_grid_services_active" @property - def is_on(self): + def is_on(self) -> bool: """Grid services is active.""" - return self.coordinator.data[POWERWALL_API_GRID_SERVICES_ACTIVE] + return self.data.grid_services_active class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall grid status sensor.""" - @property - def name(self): - """Device Name.""" - return "Grid Status" - - @property - def device_class(self): - """Device Class.""" - return BinarySensorDeviceClass.POWER + _attr_name = "Grid Status" + _attr_device_class = BinarySensorDeviceClass.POWER @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_grid_status" @property - def is_on(self): + def is_on(self) -> bool: """Grid is online.""" - return self.coordinator.data[POWERWALL_API_GRID_STATUS] == GridStatus.CONNECTED + return self.data.grid_status == GridStatus.CONNECTED class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall charging status sensor.""" - @property - def name(self): - """Device Name.""" - return "Powerwall Charging" + _attr_name = "Powerwall Charging" + _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING @property - def device_class(self): - """Device Class.""" - return BinarySensorDeviceClass.BATTERY_CHARGING + def available(self) -> bool: + """Powerwall is available.""" + # Return False if no battery is installed + return ( + super().available + and self.data.meters.get_meter(MeterType.BATTERY) is not None + ) @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_powerwall_charging" @property - def is_on(self): + def is_on(self) -> bool: """Powerwall is charging.""" # is_sending_to returns true for values greater than 100 watts - return ( - self.coordinator.data[POWERWALL_API_METERS] - .get_meter(MeterType.BATTERY) - .is_sending_to() - ) + return self.data.meters.get_meter(MeterType.BATTERY).is_sending_to() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index cb9929a890e14..08e9f90df1b82 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Tesla Powerwall integration.""" +from __future__ import annotations + import logging +from typing import Any from tesla_powerwall import ( AccessDeniedError, MissingAttributeError, Powerwall, PowerwallUnreachableError, + SiteInfo, ) import voluptuous as vol @@ -13,21 +17,25 @@ from homeassistant.components import dhcp from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_ip_address from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def _login_and_fetch_site_info(power_wall: Powerwall, password: str): +def _login_and_fetch_site_info( + power_wall: Powerwall, password: str +) -> tuple[SiteInfo, str]: """Login to the powerwall and fetch the base info.""" if password is not None: power_wall.login(password) - power_wall.detect_and_pin_version() - return power_wall.get_site_info() + return power_wall.get_site_info(), power_wall.get_gateway_din() -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, str] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from schema with values provided by the user. @@ -37,7 +45,7 @@ async def validate_input(hass: core.HomeAssistant, data): password = data[CONF_PASSWORD] try: - site_info = await hass.async_add_executor_job( + site_info, gateway_din = await hass.async_add_executor_job( _login_and_fetch_site_info, power_wall, password ) except MissingAttributeError as err: @@ -46,7 +54,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise WrongVersion from err # Return info that you want to store in the config entry. - return {"title": site_info.site_name} + return {"title": site_info.site_name, "unique_id": gateway_din.upper()} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -54,43 +62,113 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the powerwall flow.""" - self.ip_address = None + self.ip_address: str | None = None + self.title: str | None = None + self.reauth_entry: config_entries.ConfigEntry | None = None async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip - self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) - self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} - return await self.async_step_user() + gateway_din = discovery_info.hostname.upper() + # The hostname is the gateway_din (unique_id) + await self.async_set_unique_id(gateway_din) + self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: self.ip_address}) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_IP_ADDRESS] == discovery_info.ip: + if entry.unique_id is not None and is_ip_address(entry.unique_id): + if self.hass.config_entries.async_update_entry( + entry, unique_id=gateway_din + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + self.context["title_placeholders"] = { + "name": gateway_din, + "ip_address": self.ip_address, + } + errors, info = await self._async_try_connect( + {CONF_IP_ADDRESS: self.ip_address, CONF_PASSWORD: gateway_din[-5:]} + ) + if errors: + if CONF_PASSWORD in errors: + # The default password is the gateway din last 5 + # if it does not work, we have to ask + return await self.async_step_user() + return self.async_abort(reason="cannot_connect") + assert info is not None + self.title = info["title"] + return await self.async_step_confirm_discovery() + + async def _async_try_connect( + self, user_input: dict[str, Any] + ) -> tuple[dict[str, Any] | None, dict[str, str] | None]: + """Try to connect to the powerwall.""" + info = None + errors: dict[str, str] = {} + try: + info = await validate_input(self.hass, user_input) + except PowerwallUnreachableError: + errors[CONF_IP_ADDRESS] = "cannot_connect" + except WrongVersion: + errors["base"] = "wrong_version" + except AccessDeniedError: + errors[CONF_PASSWORD] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return errors, info + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm a discovered powerwall.""" + assert self.ip_address is not None + assert self.unique_id is not None + if user_input is not None: + assert self.title is not None + return self.async_create_entry( + title=self.title, + data={ + CONF_IP_ADDRESS: self.ip_address, + CONF_PASSWORD: self.unique_id[-5:], + }, + ) + + self._set_confirm_only() + self.context["title_placeholders"] = { + "name": self.title, + "ip_address": self.ip_address, + } + return self.async_show_form( + step_id="confirm_discovery", + data_schema=vol.Schema({}), + description_placeholders={ + "name": self.title, + "ip_address": self.ip_address, + }, + ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] | None = {} if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except PowerwallUnreachableError: - errors[CONF_IP_ADDRESS] = "cannot_connect" - except WrongVersion: - errors["base"] = "wrong_version" - except AccessDeniedError: - errors[CONF_PASSWORD] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - + errors, info = await self._async_try_connect(user_input) if not errors: - existing_entry = await self.async_set_unique_id( - user_input[CONF_IP_ADDRESS] - ) - if existing_entry: - self.hass.config_entries.async_update_entry( - existing_entry, data=user_input + assert info is not None + if info["unique_id"]: + await self.async_set_unique_id( + info["unique_id"], raise_on_progress=False ) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} + ) + self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( @@ -104,10 +182,36 @@ async def async_step_user(self, user_input=None): errors=errors, ) - async def async_step_reauth(self, data): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth confirmation.""" + assert self.reauth_entry is not None + errors: dict[str, str] | None = {} + if user_input is not None: + entry_data = self.reauth_entry.data + errors, _ = await self._async_try_connect( + {CONF_IP_ADDRESS: entry_data[CONF_IP_ADDRESS], **user_input} + ) + if not errors: + self.hass.config_entries.async_update_entry( + self.reauth_entry, data={**entry_data, **user_input} + ) + 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="reauth_confirm", + data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}), + errors=errors, + ) + + async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: """Handle configuration by re-auth.""" - self.ip_address = data[CONF_IP_ADDRESS] - return await self.async_step_user() + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() class WrongVersion(exceptions.HomeAssistantError): diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 8cc0cbc27cd94..b2738bce4ac44 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -1,33 +1,20 @@ """Constants for the Tesla Powerwall integration.""" +from typing import Final DOMAIN = "powerwall" -POWERWALL_OBJECT = "powerwall" -POWERWALL_COORDINATOR = "coordinator" -POWERWALL_API_CHANGED = "api_changed" +POWERWALL_BASE_INFO: Final = "base_info" +POWERWALL_COORDINATOR: Final = "coordinator" +POWERWALL_API_CHANGED: Final = "api_changed" +POWERWALL_HTTP_SESSION: Final = "http_session" +POWERWALL_LOGIN_FAILED_COUNT: Final = "login_failed_count" -UPDATE_INTERVAL = 30 +UPDATE_INTERVAL = 5 ATTR_FREQUENCY = "frequency" ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" ATTR_INSTANT_TOTAL_CURRENT = "instant_total_current" ATTR_IS_ACTIVE = "is_active" -STATUS_VERSION = "version" - -POWERWALL_SITE_NAME = "site_name" - -POWERWALL_API_METERS = "meters" -POWERWALL_API_CHARGE = "charge" -POWERWALL_API_GRID_SERVICES_ACTIVE = "grid_services_active" -POWERWALL_API_GRID_STATUS = "grid_status" -POWERWALL_API_SITEMASTER = "sitemaster" -POWERWALL_API_STATUS = "status" -POWERWALL_API_DEVICE_TYPE = "device_type" -POWERWALL_API_SITE_INFO = "site_info" -POWERWALL_API_SERIAL_NUMBERS = "serial_numbers" - -POWERWALL_HTTP_SESSION = "http_session" - MODEL = "PowerWall 2" MANUFACTURER = "Tesla" diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index ae647a080c0a8..20871944663c2 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -3,30 +3,37 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER, MODEL +from .const import ( + DOMAIN, + MANUFACTURER, + MODEL, + POWERWALL_BASE_INFO, + POWERWALL_COORDINATOR, +) +from .models import PowerwallData, PowerwallRuntimeData -class PowerWallEntity(CoordinatorEntity): +class PowerWallEntity(CoordinatorEntity[PowerwallData]): """Base class for powerwall entities.""" - def __init__( - self, coordinator, site_info, status, device_type, powerwalls_serial_numbers - ): - """Initialize the sensor.""" + def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: + """Initialize the entity.""" + base_info = powerwall_data[POWERWALL_BASE_INFO] + coordinator = powerwall_data[POWERWALL_COORDINATOR] + assert coordinator is not None super().__init__(coordinator) - self._site_info = site_info - self._device_type = device_type - self._version = status.version # The serial numbers of the powerwalls are unique to every site - self.base_unique_id = "_".join(powerwalls_serial_numbers) - - @property - def device_info(self) -> DeviceInfo: - """Powerwall device info.""" - return DeviceInfo( + self.base_unique_id = "_".join(base_info.serial_numbers) + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.base_unique_id)}, manufacturer=MANUFACTURER, - model=f"{MODEL} ({self._device_type.name})", - name=self._site_info.site_name, - sw_version=self._version, + model=f"{MODEL} ({base_info.device_type.name})", + name=base_info.site_info.site_name, + sw_version=base_info.status.version, + configuration_url=base_info.url, ) + + @property + def data(self) -> PowerwallData: + """Return the coordinator data.""" + return self.coordinator.data diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 5dcccadb681ee..be5d4678e27b4 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,17 +3,13 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.12"], + "requirements": ["tesla-powerwall==0.3.17"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { - "hostname": "1118431-*", - "macaddress": "88DA1A*" - }, - { - "hostname": "1118431-*", - "macaddress": "000145*" + "hostname": "1118431-*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tesla_powerwall"] } diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py new file mode 100644 index 0000000000000..472d9e593045c --- /dev/null +++ b/homeassistant/components/powerwall/models.py @@ -0,0 +1,50 @@ +"""The powerwall integration models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypedDict + +from requests import Session +from tesla_powerwall import ( + DeviceType, + GridStatus, + MetersAggregates, + PowerwallStatus, + SiteInfo, + SiteMaster, +) + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class PowerwallBaseInfo: + """Base information for the powerwall integration.""" + + gateway_din: None | str + site_info: SiteInfo + status: PowerwallStatus + device_type: DeviceType + serial_numbers: list[str] + url: str + + +@dataclass +class PowerwallData: + """Point in time data for the powerwall integration.""" + + charge: float + site_master: SiteMaster + meters: MetersAggregates + grid_services_active: bool + grid_status: GridStatus + + +class PowerwallRuntimeData(TypedDict): + """Run time data for the powerwall.""" + + coordinator: DataUpdateCoordinator | None + login_failed_count: int + base_info: PowerwallBaseInfo + api_changed: bool + http_session: Session diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index da0e7e7f59978..93b3c64d18cc3 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -1,9 +1,9 @@ -"""Support for August sensors.""" +"""Support for powerwall sensors.""" from __future__ import annotations -import logging +from typing import Any -from tesla_powerwall import MeterType +from tesla_powerwall import Meter, MeterType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,22 +21,13 @@ ATTR_INSTANT_TOTAL_CURRENT, ATTR_IS_ACTIVE, DOMAIN, - POWERWALL_API_CHARGE, - POWERWALL_API_DEVICE_TYPE, - POWERWALL_API_METERS, - POWERWALL_API_SERIAL_NUMBERS, - POWERWALL_API_SITE_INFO, - POWERWALL_API_STATUS, POWERWALL_COORDINATOR, ) from .entity import PowerWallEntity +from .models import PowerwallData, PowerwallRuntimeData _METER_DIRECTION_EXPORT = "export" _METER_DIRECTION_IMPORT = "import" -_METER_DIRECTIONS = [_METER_DIRECTION_EXPORT, _METER_DIRECTION_IMPORT] - - -_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -44,49 +35,28 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the August sensors.""" - powerwall_data = hass.data[DOMAIN][config_entry.entry_id] - _LOGGER.debug("Powerwall_data: %s", powerwall_data) - + """Set up the powerwall sensors.""" + powerwall_data: PowerwallRuntimeData = hass.data[DOMAIN][config_entry.entry_id] coordinator = powerwall_data[POWERWALL_COORDINATOR] - site_info = powerwall_data[POWERWALL_API_SITE_INFO] - device_type = powerwall_data[POWERWALL_API_DEVICE_TYPE] - status = powerwall_data[POWERWALL_API_STATUS] - powerwalls_serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] - - entities: list[SensorEntity] = [] - # coordinator.data[POWERWALL_API_METERS].meters holds all meters that are available - for meter in coordinator.data[POWERWALL_API_METERS].meters: - entities.append( - PowerWallEnergySensor( - meter, - coordinator, - site_info, - status, - device_type, - powerwalls_serial_numbers, - ) + assert coordinator is not None + data: PowerwallData = coordinator.data + entities: list[ + PowerWallEnergySensor + | PowerWallImportSensor + | PowerWallExportSensor + | PowerWallChargeSensor + ] = [PowerWallChargeSensor(powerwall_data)] + + for meter in data.meters.meters: + entities.extend( + [ + PowerWallEnergySensor(powerwall_data, meter), + PowerWallExportSensor(powerwall_data, meter), + PowerWallImportSensor(powerwall_data, meter), + ] ) - for meter_direction in _METER_DIRECTIONS: - entities.append( - PowerWallEnergyDirectionSensor( - meter, - coordinator, - site_info, - status, - device_type, - powerwalls_serial_numbers, - meter_direction, - ) - ) - - entities.append( - PowerWallChargeSensor( - coordinator, site_info, status, device_type, powerwalls_serial_numbers - ) - ) - async_add_entities(entities, True) + async_add_entities(entities) class PowerWallChargeSensor(PowerWallEntity, SensorEntity): @@ -98,14 +68,14 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY @property - def unique_id(self): + def unique_id(self) -> str: """Device Uniqueid.""" return f"{self.base_unique_id}_charge" @property - def native_value(self): + def native_value(self) -> int: """Get the current value in percentage.""" - return round(self.coordinator.data[POWERWALL_API_CHARGE]) + return round(self.data.charge) class PowerWallEnergySensor(PowerWallEntity, SensorEntity): @@ -115,19 +85,9 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): _attr_native_unit_of_measurement = POWER_KILO_WATT _attr_device_class = SensorDeviceClass.POWER - def __init__( - self, - meter: MeterType, - coordinator, - site_info, - status, - device_type, - powerwalls_serial_numbers, - ): + def __init__(self, powerwall_data: PowerwallRuntimeData, meter: MeterType) -> None: """Initialize the sensor.""" - super().__init__( - coordinator, site_info, status, device_type, powerwalls_serial_numbers - ) + super().__init__(powerwall_data) self._meter = meter self._attr_name = f"Powerwall {self._meter.value.title()} Now" self._attr_unique_id = ( @@ -135,18 +95,14 @@ def __init__( ) @property - def native_value(self): + def native_value(self) -> float: """Get the current value in kW.""" - return ( - self.coordinator.data[POWERWALL_API_METERS] - .get_meter(self._meter) - .get_power(precision=3) - ) + return self.data.meters.get_meter(self._meter).get_power(precision=3) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" - meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) + meter = self.data.meters.get_meter(self._meter) return { ATTR_FREQUENCY: round(meter.frequency, 1), ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), @@ -158,37 +114,67 @@ def extra_state_attributes(self): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_state_class = SensorStateClass.TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY def __init__( self, + powerwall_data: PowerwallRuntimeData, meter: MeterType, - coordinator, - site_info, - status, - device_type, - powerwalls_serial_numbers, - meter_direction, - ): + meter_direction: str, + ) -> None: """Initialize the sensor.""" - super().__init__( - coordinator, site_info, status, device_type, powerwalls_serial_numbers - ) + super().__init__(powerwall_data) self._meter = meter - self._meter_direction = meter_direction - self._attr_name = ( - f"Powerwall {self._meter.value.title()} {self._meter_direction.title()}" - ) - self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_{self._meter_direction}" - ) + self._attr_name = f"Powerwall {meter.value.title()} {meter_direction.title()}" + self._attr_unique_id = f"{self.base_unique_id}_{meter.value}_{meter_direction}" + + @property + def available(self) -> bool: + """Check if the reading is actually available. + + The device reports 0 when something goes wrong which + we do not want to include in statistics and its a + transient data error. + """ + return super().available and self.native_value != 0 + + @property + def meter(self) -> Meter: + """Get the meter for the sensor.""" + return self.data.meters.get_meter(self._meter) + + +class PowerWallExportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Export sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_EXPORT) + + @property + def native_value(self) -> float: + """Get the current value in kWh.""" + return self.meter.get_energy_exported() + + +class PowerWallImportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Import sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_IMPORT) @property - def native_value(self): + def native_value(self) -> float: """Get the current value in kWh.""" - meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) - if self._meter_direction == _METER_DIRECTION_EXPORT: - return meter.get_energy_exported() - return meter.get_energy_imported() + return self.meter.get_energy_imported() diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index e1b2f2dbd3bec..8995a0f956eb5 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { "user": { "title": "Connect to the powerwall", @@ -9,6 +9,17 @@ "ip_address": "[%key:common::config_flow::data::ip%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confim": { + "title": "Reauthenticate the powerwall", + "description": "[%key:component::powerwall::config::step::user::description%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, + "confirm_discovery": { + "title": "[%key:component::powerwall::config::step::user::title%]", + "description": "Do you want to setup {name} ({ip_address})?" } }, "error": { @@ -18,6 +29,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index cef3726d75967..12186a54eec19 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -1,6 +1,17 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { + "confirm_discovery": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({ip_address})?" + }, + "reauth_confim": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index c8020069676a6..bbba4d0bf5eed 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Vols configurar {name} ({ip_address})?", + "title": "Connexi\u00f3 amb el Powerwall" + }, + "reauth_confim": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya normalment s\u00f3n els darrers cinc car\u00e0cters del n\u00famero de s\u00e8rie de la pasarel\u00b7la (backup gateway) i es pot trobar a l'aplicaci\u00f3 de Tesla. Tamb\u00e9 s\u00f3n els darrers 5 car\u00e0cters de la contrasenya que es troba a l'interior de la tapa de la pasarel\u00b7la vers\u00f3 2 (backup gateway 2).", + "title": "Re-autenticaci\u00f3 del powerwall" + }, "user": { "data": { "ip_address": "Adre\u00e7a IP", diff --git a/homeassistant/components/powerwall/translations/cs.json b/homeassistant/components/powerwall/translations/cs.json index d6e5cd5904b00..6f6ffccd4777f 100644 --- a/homeassistant/components/powerwall/translations/cs.json +++ b/homeassistant/components/powerwall/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { @@ -10,8 +11,16 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "wrong_version": "Powerwall pou\u017e\u00edv\u00e1 verzi softwaru, kter\u00e1 nen\u00ed podporov\u00e1na. Zva\u017ete upgrade nebo nahlaste probl\u00e9m, aby mohl b\u00fdt vy\u0159e\u0161en." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "title": "P\u0159ipojen\u00ed k powerwall" + }, + "reauth_confim": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "ip_address": "IP adresa", diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 8ac8a1a5b1d6e..17f852542080b 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Unerwarteter Fehler", "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "M\u00f6chtest du {name} ({ip_address}) einrichten?", + "title": "Powerwall verbinden" + }, + "reauth_confim": { + "data": { + "password": "Passwort" + }, + "description": "Das Kennwort ist in der Regel die letzten 5 Zeichen der Seriennummer des Backup Gateway und kann in der Tesla-App gefunden werden oder es sind die letzten 5 Zeichen des Kennworts, das sich in der T\u00fcr f\u00fcr Backup Gateway 2 befindet.", + "title": "Powerwall erneut authentifizieren" + }, "user": { "data": { "ip_address": "IP-Adresse", diff --git a/homeassistant/components/powerwall/translations/el.json b/homeassistant/components/powerwall/translations/el.json index 79e3178f46f37..d3649e44a8d3c 100644 --- a/homeassistant/components/powerwall/translations/el.json +++ b/homeassistant/components/powerwall/translations/el.json @@ -1,8 +1,34 @@ { "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", + "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", + "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", + "wrong_version": "\u03a4\u03bf powerwall \u03c3\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u03a3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03c5\u03b8\u03b5\u03af." + }, "flow_title": "{ip_address}", "step": { + "confirm_discovery": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf powerwall" + }, + "reauth_confim": { + "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 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bf\u03b9 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf\u03b9 5 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03bf Backup Gateway \u03ba\u03b1\u03b9 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Tesla \u03ae \u03bf\u03b9 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf\u03b9 5 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03c3\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03c4\u03b7\u03c2 \u03c0\u03cc\u03c1\u03c4\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Backup Gateway 2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 powerwall" + }, "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": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bf\u03b9 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf\u03b9 5 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03bf Backup Gateway \u03ba\u03b1\u03b9 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Tesla \u03ae \u03bf\u03b9 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03bf\u03b9 5 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03c3\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03c4\u03b7\u03c2 \u03c0\u03cc\u03c1\u03c4\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Backup Gateway 2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf powerwall" } diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 3be711d94c548..04279759888fc 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", "reauth_successful": "Re-authentication was successful" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Do you want to setup {name} ({ip_address})?", + "title": "Connect to the powerwall" + }, + "reauth_confim": { + "data": { + "password": "Password" + }, + "description": "The password is usually the last 5 characters of the serial number for Backup Gateway and can be found in the Tesla app or the last 5 characters of the password found inside the door for Backup Gateway 2.", + "title": "Reauthenticate the powerwall" + }, "user": { "data": { "ip_address": "IP Address", diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index f2beb19d5dac9..767f77e58bdd8 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -12,6 +12,9 @@ }, "flow_title": "Powerwall de Tesla ({ip_address})", "step": { + "reauth_confim": { + "title": "Reautorizar la powerwall" + }, "user": { "data": { "ip_address": "Direcci\u00f3n IP", diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 98eb25ca17af6..511bc7825d8af 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Kas soovid seadistada {name} ({ip_address})?", + "title": "\u00dchendu Powerwalliga" + }, + "reauth_confim": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Salas\u00f5naks on tavaliselt Backup Gateway seerianumbri 5 viimast t\u00e4rki mille leiab Tesla rakendusest v\u00f5i 5 viimast t\u00e4rki Backup Gateway 2 ukse sisek\u00fcljel.", + "title": "Taasautendi Powerwall" + }, "user": { "data": { "ip_address": "IP aadress", diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index a6a6edab93824..1f5d5ac22cd15 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, - "flow_title": "{ip_address}", + "flow_title": "{nom} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Voulez-vous configurer {name} ({ip_address})?", + "title": "Connectez-vous au Powerwall" + }, + "reauth_confim": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe est g\u00e9n\u00e9ralement les 5 derniers caract\u00e8res du num\u00e9ro de s\u00e9rie de Backup Gateway et peut \u00eatre trouv\u00e9 dans l\u2019application Tesla ou les 5 derniers caract\u00e8res du mot de passe trouv\u00e9 \u00e0 l\u2019int\u00e9rieur de la porte pour la passerelle de Backup Gateway 2.", + "title": "R\u00e9authentifier le powerwall" + }, "user": { "data": { "ip_address": "Adresse IP", diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json index f090e85c0cfa3..c4a69c402c48a 100644 --- a/homeassistant/components/powerwall/translations/he.json +++ b/homeassistant/components/powerwall/translations/he.json @@ -2,6 +2,7 @@ "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", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { @@ -11,6 +12,12 @@ }, "flow_title": "{ip_address}", "step": { + "reauth_confim": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2." + }, "user": { "data": { "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 8975694ca9512..6f53b1ef575f4 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { @@ -10,8 +11,19 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ip_address})?", + "title": "Csatlakoz\u00e1s a powerwallhoz" + }, + "reauth_confim": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A jelsz\u00f3 \u00e1ltal\u00e1ban a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g sorozatsz\u00e1m\u00e1nak utols\u00f3 5 karaktere, \u00e9s megtal\u00e1lhat\u00f3 a Tesla alkalmaz\u00e1sban, vagy a jelsz\u00f3 utols\u00f3 5 karaktere a Biztons\u00e1gi ment\u00e9s k\u00f6zponti egys\u00e9g 2 ajtaj\u00e1ban.", + "title": "A powerwall \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "ip_address": "IP c\u00edm", diff --git a/homeassistant/components/powerwall/translations/id.json b/homeassistant/components/powerwall/translations/id.json index 95f8d6009018a..eeba049a9f2cc 100644 --- a/homeassistant/components/powerwall/translations/id.json +++ b/homeassistant/components/powerwall/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Kesalahan yang tidak diharapkan", "wrong_version": "Powerwall Anda menggunakan versi perangkat lunak yang tidak didukung. Pertimbangkan untuk memutakhirkan atau melaporkan masalah ini agar dapat diatasi." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Ingin menyiapkan {name} ({ip_address})?", + "title": "Hubungkan ke powerwall" + }, + "reauth_confim": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi umumnya adalah 5 karakter terakhir dari nomor seri untuk Backup Gateway dan dapat ditemukan di aplikasi Tesla atau 5 karakter terakhir kata sandi yang ditemukan di dalam pintu untuk Backup Gateway 2.", + "title": "Autentikasi ulang powerwall" + }, "user": { "data": { "ip_address": "Alamat IP", diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 15a76c31f4846..889356fe8a17e 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { @@ -10,15 +11,26 @@ "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Considera l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Vuoi configurare {name} ({ip_address})?", + "title": "Connettiti al powerwall" + }, + "reauth_confim": { + "data": { + "password": "Password" + }, + "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'applicazione Tesla o dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", + "title": "Nuova autenticazione powerwall" + }, "user": { "data": { "ip_address": "Indirizzo IP", "password": "Password" }, - "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'app Tesla; oppure dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", - "title": "Connessione al Powerwall" + "description": "La password di solito \u00e8 costituita dagli ultimi 5 caratteri del numero di serie per il Backup Gateway e pu\u00f2 essere trovata nell'applicazione Tesla o dagli ultimi 5 caratteri della password trovata all'interno della porta per il Backup Gateway 2.", + "title": "Connettiti al powerwall" } } } diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index c53bbe573ec50..be7078143b05e 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { @@ -12,6 +13,17 @@ }, "flow_title": "{ip_address}", "step": { + "confirm_discovery": { + "description": "{name} ({ip_address}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "Powerwall\u306b\u63a5\u7d9a" + }, + "reauth_confim": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", + "title": "powerwall\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index 87b78e719d0e2..007d4e9e86b9f 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." }, - "flow_title": "({ip_adres})", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Wilt u {name} ({ip_address}) instellen?", + "title": "Maak verbinding met de powerwall" + }, + "reauth_confim": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord is meestal de laatste 5 tekens van het serienummer voor Backup Gateway en is te vinden in de Tesla-app of de laatste 5 tekens van het wachtwoord aan de binnenkant van de deur voor Backup Gateway 2.", + "title": "De powerwall opnieuw verifi\u00ebren" + }, "user": { "data": { "ip_address": "IP-adres", diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 6f45fb144f53b..01cf58c6ed4ca 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ( {ip_address} )", "step": { + "confirm_discovery": { + "description": "Vil du konfigurere {name} ( {ip_address} )?", + "title": "Koble til powerwall" + }, + "reauth_confim": { + "data": { + "password": "Passord" + }, + "description": "Passordet er vanligvis de siste 5 tegnene i serienummeret for Backup Gateway, og kan bli funnet i Tesla-appen eller de siste 5 tegnene i passordet som er funnet inne i d\u00f8ren til Backup Gateway 2.", + "title": "Autentiser powerwallen p\u00e5 nytt" + }, "user": { "data": { "ip_address": "IP adresse", diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 8d4f82fa14eab..9c56edf13b9e5 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({ip_address})?", + "title": "Po\u0142\u0105czenie z Powerwall" + }, + "reauth_confim": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o to zazwyczaj 5 ostatnich znak\u00f3w numeru seryjnego Backup Gateway i mo\u017cna je znale\u017a\u0107 w aplikacji Tesla; lub ostatnie 5 znak\u00f3w has\u0142a na wewn\u0119trznej stronie drzwiczek Backup Gateway 2.", + "title": "Ponownie uwierzytelnij powerwall" + }, "user": { "data": { "ip_address": "Adres IP", diff --git a/homeassistant/components/powerwall/translations/pt-BR.json b/homeassistant/components/powerwall/translations/pt-BR.json index e97b93d1e6662..82d64eff57527 100644 --- a/homeassistant/components/powerwall/translations/pt-BR.json +++ b/homeassistant/components/powerwall/translations/pt-BR.json @@ -1,17 +1,35 @@ { "config": { "abort": { - "already_configured": "O powerwall j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", - "unknown": "Erro inesperado" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado", + "wrong_version": "Seu powerwall usa uma vers\u00e3o de software que n\u00e3o \u00e9 compat\u00edvel. Considere atualizar ou relatar este problema para que ele possa ser resolvido." }, + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Deseja configurar {name} ({ip_address})?", + "title": "Conecte-se ao powerwall" + }, + "reauth_confim": { + "data": { + "password": "Senha" + }, + "description": "A senha \u00e9 geralmente os \u00faltimos 5 caracteres do n\u00famero de s\u00e9rie do Backup Gateway e pode ser encontrada no aplicativo Tesla ou os \u00faltimos 5 caracteres da senha encontrada dentro da porta do Backup Gateway 2.", + "title": "Reautentique o powerwall" + }, "user": { "data": { - "ip_address": "Endere\u00e7o IP" + "ip_address": "Endere\u00e7o IP", + "password": "Senha" }, + "description": "A senha \u00e9 geralmente os \u00faltimos 5 caracteres do n\u00famero de s\u00e9rie do Backup Gateway e pode ser encontrada no aplicativo Tesla ou os \u00faltimos 5 caracteres da senha encontrada dentro da porta do Backup Gateway 2.", "title": "Conecte-se ao powerwall" } } diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index f8299a59445aa..e96897ad1df26 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -2,6 +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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "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": { @@ -10,8 +11,19 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({ip_address})?", + "title": "Tesla Powerwall" + }, + "reauth_confim": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u044b\u0447\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0434\u043b\u044f Backup Gateway, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 Telsa; \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 5 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043f\u0430\u0440\u043e\u043b\u044f, \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u043d\u0443\u0442\u0440\u0438 Backup Gateway 2.", + "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": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/powerwall/translations/sk.json b/homeassistant/components/powerwall/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/powerwall/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json index a243e22b56610..48c076d8eedac 100644 --- a/homeassistant/components/powerwall/translations/tr.json +++ b/homeassistant/components/powerwall/translations/tr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -10,8 +11,19 @@ "unknown": "Beklenmeyen hata", "wrong_version": "G\u00fc\u00e7 duvar\u0131n\u0131z desteklenmeyen bir yaz\u0131l\u0131m s\u00fcr\u00fcm\u00fc kullan\u0131yor. \u00c7\u00f6z\u00fclebilmesi i\u00e7in l\u00fctfen bu sorunu y\u00fckseltmeyi veya bildirmeyi d\u00fc\u015f\u00fcn\u00fcn." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "{name} ( {ip_address} ) kurulumu yapmak istiyor musunuz?", + "title": "Powerwall'a ba\u011flan\u0131n" + }, + "reauth_confim": { + "data": { + "password": "Parola" + }, + "description": "Parola genellikle Backup Gateway i\u00e7in seri numaras\u0131n\u0131n son 5 karakteridir ve Tesla uygulamas\u0131nda veya Backup Gateway 2 i\u00e7in kap\u0131n\u0131n i\u00e7inde bulunan parolan\u0131n son 5 karakterinde bulunabilir.", + "title": "Powerwall'\u0131 yeniden do\u011frulay\u0131n" + }, "user": { "data": { "ip_address": "\u0130p Adresi", diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json index 9b397138c5272..cee740b4d50e1 100644 --- a/homeassistant/components/powerwall/translations/uk.json +++ b/homeassistant/components/powerwall/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", @@ -9,6 +10,11 @@ "wrong_version": "\u0412\u0430\u0448 Powerwall \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u0432\u0435\u0440\u0441\u0456\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u043e\u0433\u043e \u0437\u0430\u0431\u0435\u0437\u043f\u0435\u0447\u0435\u043d\u043d\u044f, \u044f\u043a\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0440\u043e\u0437\u0433\u043b\u044f\u043d\u044c\u0442\u0435 \u043c\u043e\u0436\u043b\u0438\u0432\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0430\u0431\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0442\u0435 \u043f\u0440\u043e \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u0432\u0438\u0440\u0456\u0448\u0438\u0442\u0438." }, "step": { + "reauth_confim": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 21a2a4f215953..0f3b63d4b45c0 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { @@ -10,8 +11,19 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({ip_address})\uff1f", + "title": "\u9023\u7dda\u81f3 Powerwall" + }, + "reauth_confim": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u5bc6\u78bc\u901a\u5e38\u70ba\u81f3\u5c11\u5099\u4efd\u9598\u9053\u5668\u5e8f\u865f\u7684\u6700\u5f8c\u4e94\u78bc\uff0c\u4e26\u4e14\u80fd\u5920\u65bc Telsa App \u4e2d\n\u627e\u5230\u3002\u6216\u8005\u70ba\u5099\u4efd\u9598\u9053\u5668 2 \u9580\u5167\u5074\u627e\u5230\u7684\u5bc6\u78bc\u6700\u5f8c\u4e94\u78bc\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 Powerwall" + }, "user": { "data": { "ip_address": "IP \u4f4d\u5740", diff --git a/homeassistant/components/profiler/translations/el.json b/homeassistant/components/profiler/translations/el.json new file mode 100644 index 0000000000000..adba199dc48ac --- /dev/null +++ b/homeassistant/components/profiler/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/pt-BR.json b/homeassistant/components/profiler/translations/pt-BR.json new file mode 100644 index 0000000000000..7caf798371445 --- /dev/null +++ b/homeassistant/components/profiler/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/uk.json b/homeassistant/components/profiler/translations/uk.json index 5594895456e98..b1d6150a318d8 100644 --- a/homeassistant/components/profiler/translations/uk.json +++ b/homeassistant/components/profiler/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { "user": { diff --git a/homeassistant/components/profiler/translations/zh-Hant.json b/homeassistant/components/profiler/translations/zh-Hant.json index c7d73c344d8b3..e10e609a118ee 100644 --- a/homeassistant/components/profiler/translations/zh-Hant.json +++ b/homeassistant/components/profiler/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/manifest.json b/homeassistant/components/progettihwsw/manifest.json index d1dbb30f2fcb2..ca4ff88c986a5 100644 --- a/homeassistant/components/progettihwsw/manifest.json +++ b/homeassistant/components/progettihwsw/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@ardaseremet"], "requirements": ["progettihwsw==0.1.1"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ProgettiHWSW"] } diff --git a/homeassistant/components/progettihwsw/translations/el.json b/homeassistant/components/progettihwsw/translations/el.json index e2d0eeae56103..2e345a75b4463 100644 --- a/homeassistant/components/progettihwsw/translations/el.json +++ b/homeassistant/components/progettihwsw/translations/el.json @@ -1,5 +1,12 @@ { "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" + }, "step": { "relay_modes": { "data": { @@ -24,6 +31,7 @@ }, "user": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1" diff --git a/homeassistant/components/progettihwsw/translations/pt-BR.json b/homeassistant/components/progettihwsw/translations/pt-BR.json new file mode 100644 index 0000000000000..9d5f4715fd1cf --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "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" + }, + "title": "Configurar rel\u00e9s" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "title": "Placa de configura\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/sk.json b/homeassistant/components/progettihwsw/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index e5f2fc056dc3a..0d035d969dc1c 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": ["proliphix==0.4.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["proliphix"] } diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 9315bf308b7a8..0dfdd03e5e571 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -5,5 +5,6 @@ "requirements": ["prometheus_client==0.7.1"], "dependencies": ["http"], "codeowners": ["@knyar"], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["prometheus_client"] } diff --git a/homeassistant/components/prosegur/manifest.json b/homeassistant/components/prosegur/manifest.json index 853324c940814..ecb3a9e6c415a 100644 --- a/homeassistant/components/prosegur/manifest.json +++ b/homeassistant/components/prosegur/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@dgomes" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyprosegur"] } diff --git a/homeassistant/components/prosegur/translations/el.json b/homeassistant/components/prosegur/translations/el.json index c5dee661aa25a..d76c733603dd0 100644 --- a/homeassistant/components/prosegur/translations/el.json +++ b/homeassistant/components/prosegur/translations/el.json @@ -1,9 +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", + "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", + "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": { "reauth_confirm": { "data": { - "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Prosegur." + "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Prosegur.", + "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" + } + }, + "user": { + "data": { + "country": "\u03a7\u03ce\u03c1\u03b1", + "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/prosegur/translations/nb.json b/homeassistant/components/prosegur/translations/nb.json new file mode 100644 index 0000000000000..c106bc179b317 --- /dev/null +++ b/homeassistant/components/prosegur/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/pt-BR.json b/homeassistant/components/prosegur/translations/pt-BR.json new file mode 100644 index 0000000000000..71d9df7c17571 --- /dev/null +++ b/homeassistant/components/prosegur/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "description": "Re-autentique com a conta Prosegur.", + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "user": { + "data": { + "country": "Pa\u00eds", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/sk.json b/homeassistant/components/prosegur/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/prosegur/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index da4392ccc09b8..8eec3e2f0383a 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -70,14 +70,14 @@ def setup_proximity_component( hass: HomeAssistant, name: str, config: ConfigType ) -> bool: """Set up the individual proximity component.""" - ignored_zones = config.get(CONF_IGNORED_ZONES) - proximity_devices = config.get(CONF_DEVICES) - tolerance = config.get(CONF_TOLERANCE) + ignored_zones: list[str] = config[CONF_IGNORED_ZONES] + proximity_devices: list[str] = config[CONF_DEVICES] + tolerance: int = config[CONF_TOLERANCE] proximity_zone = name - unit_of_measurement = config.get( + unit_of_measurement: str = config.get( CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit ) - zone_id = f"zone.{config.get(CONF_ZONE)}" + zone_id = f"zone.{config[CONF_ZONE]}" proximity = Proximity( # type:ignore[no-untyped-call] hass, diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index dfed6d623f4c2..4b600abc930c0 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/proxmoxve", "codeowners": ["@jhollowe", "@Corbeno"], "requirements": ["proxmoxer==1.1.1"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["proxmoxer"] } diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 799615a8cbab1..c7f47333901c7 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": ["pyps4-2ndscreen==1.3.1"], "codeowners": ["@ktnrg45"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyps4_2ndscreen"] } diff --git a/homeassistant/components/ps4/translations/el.json b/homeassistant/components/ps4/translations/el.json index 1f047f44ab475..5bc6b4a7b0e4b 100644 --- a/homeassistant/components/ps4/translations/el.json +++ b/homeassistant/components/ps4/translations/el.json @@ -1,11 +1,16 @@ { "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", "credential_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\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", "port_987_bind_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 987. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7](https://www.home-assistant.io/components/ps4/) \u03b3\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "port_997_bind_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 997. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7](https://www.home-assistant.io/components/ps4/) \u03b3\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2." }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "credential_timeout": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b4\u03b9\u03b1\u03c0\u03af\u03c3\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b5\u03c1\u03bc\u03ac\u03c4\u03b9\u03c3\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03b7\u03c2. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 submit \u03b3\u03b9\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7.", + "login_failed": "\u0397 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03c4\u03bf PlayStation 4 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc\u03c2.", "no_ipaddress": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 PlayStation 4 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5." }, "step": { @@ -15,14 +20,20 @@ }, "link": { "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 PlayStation 4. \u0393\u03b9\u03b1 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \"\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03ba\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 PlayStation 4. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf 'Mobile App Connection Settings' (\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac) \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Add Device' (\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2). \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7](https://www.home-assistant.io/components/ps4/) \u03b3\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP (\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u0391\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7)." + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP (\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u0391\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7).", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2" }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03af\u03bd\u03b5\u03b9 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 Auto Discovery, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b8\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 4d2390d89c585..0a7db1888f6d5 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -33,7 +33,7 @@ "ip_address": "Indirizzo IP (Lascia vuoto se stai usando il rilevamento automatico).", "mode": "Modalit\u00e0 di configurazione" }, - "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indiriizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno automaticamente individuati.", + "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno automaticamente individuati.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/ps4/translations/pt-BR.json b/homeassistant/components/ps4/translations/pt-BR.json index e0547d1f06f2e..7938cbb6241d5 100644 --- a/homeassistant/components/ps4/translations/pt-BR.json +++ b/homeassistant/components/ps4/translations/pt-BR.json @@ -1,15 +1,17 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "credential_error": "Erro ao buscar credenciais.", - "no_devices_found": "Nenhum dispositivo PlayStation 4 encontrado na rede.", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "port_987_bind_error": "N\u00e3o foi poss\u00edvel conectar na porta 987. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais.", "port_997_bind_error": "N\u00e3o foi poss\u00edvel conectar na porta 997. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais." }, "error": { + "cannot_connect": "Falha ao conectar", "credential_timeout": "Servi\u00e7o de credencial expirou. Pressione Submit para reiniciar.", - "login_failed": "N\u00e3o foi poss\u00edvel parear com o PlayStation 4. Verifique se o PIN est\u00e1 correto.", - "no_ipaddress": "Digite o endere\u00e7o IP do PlayStation 4 que voc\u00ea gostaria de configurar." + "login_failed": "N\u00e3o foi poss\u00edvel parear com o PlayStation 4. Verifique se o C\u00f3digo PIN est\u00e1 correto.", + "no_ipaddress": "Digite o Endere\u00e7o IP do PlayStation 4 que voc\u00ea gostaria de configurar." }, "step": { "creds": { @@ -18,12 +20,12 @@ }, "link": { "data": { - "code": "PIN", + "code": "C\u00f3digo PIN", "ip_address": "Endere\u00e7o IP", "name": "Nome", "region": "Regi\u00e3o" }, - "description": "Digite suas informa\u00e7\u00f5es do PlayStation 4. Para 'PIN', navegue at\u00e9 'Configura\u00e7\u00f5es' no seu console PlayStation 4. Em seguida, navegue at\u00e9 \"Configura\u00e7\u00f5es de conex\u00e3o de aplicativos m\u00f3veis\" e selecione \"Adicionar dispositivo\". Digite o PIN exibido. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais.", + "description": "Digite suas informa\u00e7\u00f5es do PlayStation 4. Para C\u00f3digo PIN, navegue at\u00e9 'Configura\u00e7\u00f5es' no seu console PlayStation 4. Em seguida, navegue at\u00e9 \"Configura\u00e7\u00f5es de conex\u00e3o de aplicativos m\u00f3veis\" e selecione \"Adicionar dispositivo\". Digite o C\u00f3digo PIN exibido. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais.", "title": "Playstation 4" }, "mode": { diff --git a/homeassistant/components/ps4/translations/sk.json b/homeassistant/components/ps4/translations/sk.json new file mode 100644 index 0000000000000..fa12207330bca --- /dev/null +++ b/homeassistant/components/ps4/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "link": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/__init__.py b/homeassistant/components/pure_energie/__init__.py new file mode 100644 index 0000000000000..4e86726ccc82d --- /dev/null +++ b/homeassistant/components/pure_energie/__init__.py @@ -0,0 +1,76 @@ +"""The Pure Energie integration.""" +from __future__ import annotations + +from typing import NamedTuple + +from gridnet import Device, GridNet, SmartBridge + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Pure Energie from a config entry.""" + + coordinator = PureEnergieDataUpdateCoordinator(hass) + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await coordinator.gridnet.close() + raise + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Pure Energie config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok + + +class PureEnergieData(NamedTuple): + """Class for defining data in dict.""" + + device: Device + smartbridge: SmartBridge + + +class PureEnergieDataUpdateCoordinator(DataUpdateCoordinator[PureEnergieData]): + """Class to manage fetching Pure Energie data from single eindpoint.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + ) -> None: + """Initialize global Pure Energie data updater.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + self.gridnet = GridNet( + self.config_entry.data[CONF_HOST], session=async_get_clientsession(hass) + ) + + async def _async_update_data(self) -> PureEnergieData: + """Fetch data from SmartBridge.""" + return PureEnergieData( + device=await self.gridnet.device(), + smartbridge=await self.gridnet.smartbridge(), + ) diff --git a/homeassistant/components/pure_energie/config_flow.py b/homeassistant/components/pure_energie/config_flow.py new file mode 100644 index 0000000000000..2b1e20d645ef6 --- /dev/null +++ b/homeassistant/components/pure_energie/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for Pure Energie integration.""" +from __future__ import annotations + +from typing import Any + +from gridnet import Device, GridNet, GridNetConnectionError +import voluptuous as vol + +from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Pure Energie integration.""" + + VERSION = 1 + discovered_host: str + discovered_device: Device + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + errors = {} + + if user_input is not None: + try: + device = await self._async_get_device(user_input[CONF_HOST]) + except GridNetConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(device.n2g_id, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]} + ) + return self.async_create_entry( + title="Pure Energie Meter", + data={ + CONF_HOST: user_input[CONF_HOST], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + } + ), + errors=errors or {}, + ) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + self.discovered_host = discovery_info.host + try: + self.discovered_device = await self._async_get_device(discovery_info.host) + except GridNetConnectionError: + return self.async_abort(reason="cannot_connect") + + await self.async_set_unique_id(self.discovered_device.n2g_id) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host}) + + self.context.update( + { + "title_placeholders": { + CONF_NAME: "Pure Energie Meter", + CONF_HOST: self.discovered_host, + "model": self.discovered_device.model, + }, + } + ) + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by zeroconf.""" + if user_input is not None: + return self.async_create_entry( + title="Pure Energie Meter", + data={ + CONF_HOST: self.discovered_host, + }, + ) + + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={ + CONF_NAME: "Pure Energie Meter", + "model": self.discovered_device.model, + }, + ) + + async def _async_get_device(self, host: str) -> Device: + """Get device information from Pure Energie device.""" + session = async_get_clientsession(self.hass) + gridnet = GridNet(host, session=session) + return await gridnet.device() diff --git a/homeassistant/components/pure_energie/const.py b/homeassistant/components/pure_energie/const.py new file mode 100644 index 0000000000000..9c908da606891 --- /dev/null +++ b/homeassistant/components/pure_energie/const.py @@ -0,0 +1,10 @@ +"""Constants for the Pure Energie integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "pure_energie" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/pure_energie/manifest.json b/homeassistant/components/pure_energie/manifest.json new file mode 100644 index 0000000000000..7997e9c4b5d2a --- /dev/null +++ b/homeassistant/components/pure_energie/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "pure_energie", + "name": "Pure Energie", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/pure_energie", + "requirements": ["gridnet==4.0.0"], + "codeowners": ["@klaasnicolaas"], + "quality_scale": "platinum", + "iot_class": "local_polling", + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "smartbridge*" + } + ] +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py new file mode 100644 index 0000000000000..fffbfd7c7bb24 --- /dev/null +++ b/homeassistant/components/pure_energie/sensor.py @@ -0,0 +1,111 @@ +"""Support for Pure Energie sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PureEnergieData, PureEnergieDataUpdateCoordinator +from .const import DOMAIN + + +@dataclass +class PureEnergieSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[PureEnergieData], int | float] + + +@dataclass +class PureEnergieSensorEntityDescription( + SensorEntityDescription, PureEnergieSensorEntityDescriptionMixin +): + """Describes a Pure Energie sensor entity.""" + + +SENSORS: tuple[PureEnergieSensorEntityDescription, ...] = ( + PureEnergieSensorEntityDescription( + key="power_flow", + name="Power Flow", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.smartbridge.power_flow, + ), + PureEnergieSensorEntityDescription( + key="energy_consumption_total", + name="Energy Consumption", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.smartbridge.energy_consumption_total, + ), + PureEnergieSensorEntityDescription( + key="energy_production_total", + name="Energy Production", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.smartbridge.energy_production_total, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Pure Energie Sensors based on a config entry.""" + async_add_entities( + PureEnergieSensorEntity( + coordinator=hass.data[DOMAIN][entry.entry_id], + description=description, + entry=entry, + ) + for description in SENSORS + ) + + +class PureEnergieSensorEntity(CoordinatorEntity[PureEnergieData], SensorEntity): + """Defines an Pure Energie sensor.""" + + coordinator: PureEnergieDataUpdateCoordinator + entity_description: PureEnergieSensorEntityDescription + + def __init__( + self, + *, + coordinator: PureEnergieDataUpdateCoordinator, + description: PureEnergieSensorEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize Pure Energie sensor.""" + super().__init__(coordinator=coordinator) + self.entity_id = f"{SENSOR_DOMAIN}.pem_{description.key}" + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.device.n2g_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data.device.n2g_id)}, + configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", + sw_version=coordinator.data.device.firmware, + manufacturer=coordinator.data.device.manufacturer, + model=coordinator.data.device.model, + name=entry.title, + ) + + @property + def native_value(self) -> int | float: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/pure_energie/strings.json b/homeassistant/components/pure_energie/strings.json new file mode 100644 index 0000000000000..356d161f006de --- /dev/null +++ b/homeassistant/components/pure_energie/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add Pure Energie Meter (`{model}`) to Home Assistant?", + "title": "Discovered Pure Energie Meter device" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/bg.json b/homeassistant/components/pure_energie/translations/bg.json new file mode 100644 index 0000000000000..93c06fc23630c --- /dev/null +++ b/homeassistant/components/pure_energie/translations/bg.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, + "zeroconf_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 Pure Energie Meter (`{model}`) \u043a\u044a\u043c Home Assistant?", + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Pure Energie Meter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/ca.json b/homeassistant/components/pure_energie/translations/ca.json new file mode 100644 index 0000000000000..cb725e87646a5 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/ca.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + }, + "zeroconf_confirm": { + "description": "Vols afegir Pure Energie Meter (`{model}`) a Home Assistant?", + "title": "Dispositiu Pure Energie Meter descobert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/de.json b/homeassistant/components/pure_energie/translations/de.json new file mode 100644 index 0000000000000..6aafb35d5f92c --- /dev/null +++ b/homeassistant/components/pure_energie/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest du Pure Energie Meter (` {model} `) zu Home Assistant hinzuf\u00fcgen?", + "title": "Pure Energie Meter-Ger\u00e4t entdeckt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/el.json b/homeassistant/components/pure_energie/translations/el.json new file mode 100644 index 0000000000000..a63ada73fa9d4 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/el.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + }, + "zeroconf_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Pure Energie Meter (`{model}`) \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Pure Energie Meter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/en.json b/homeassistant/components/pure_energie/translations/en.json new file mode 100644 index 0000000000000..6773cf51478d0 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add Pure Energie Meter (`{model}`) to Home Assistant?", + "title": "Discovered Pure Energie Meter device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json new file mode 100644 index 0000000000000..eb5d98c7ebf8c --- /dev/null +++ b/homeassistant/components/pure_energie/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya se encuentra configurado", + "cannot_connect": "Error al conectar" + }, + "error": { + "cannot_connect": "Error al conectar" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Anfitri\u00f3n" + } + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir el Medidor Pure Energie (`{name}`) a Home Assistant?", + "title": "Medidor Pure Energie encontrado" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/et.json b/homeassistant/components/pure_energie/translations/et.json new file mode 100644 index 0000000000000..4df06e2ca041d --- /dev/null +++ b/homeassistant/components/pure_energie/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{model} ( {host} )", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Kas lisada Home Assistantile Pure Energie Meteri(`{model}`)?", + "title": "Leiti Pure Energie Meter seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/he.json b/homeassistant/components/pure_energie/translations/he.json new file mode 100644 index 0000000000000..e9c083d9afc6d --- /dev/null +++ b/homeassistant/components/pure_energie/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", + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/hu.json b/homeassistant/components/pure_energie/translations/hu.json new file mode 100644 index 0000000000000..d4bd60def2c97 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/hu.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + }, + "zeroconf_confirm": { + "description": "Szeretn\u00e9 hozz\u00e1adni a Pure Energie Metert(`{model}`) term\u00e9ket Home Assistanthoz?", + "title": "Felfedezett Pure Energie Meter eszk\u00f6z" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/id.json b/homeassistant/components/pure_energie/translations/id.json new file mode 100644 index 0000000000000..9557e4bc08f4c --- /dev/null +++ b/homeassistant/components/pure_energie/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan Pure Energie Meter (`{name}`) ke Home Assistant?", + "title": "Peranti Pure Energie Meter yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json new file mode 100644 index 0000000000000..457f7cfebc0f3 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Connessione fallita" + }, + "error": { + "cannot_connect": "Connessione fallita" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere Pure Energie Meter (`{model}`) a Home Assistant?", + "title": "Scoperto dispositivo Pure Energie Meter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/ja.json b/homeassistant/components/pure_energie/translations/ja.json new file mode 100644 index 0000000000000..bb7b3fe9f1321 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "zeroconf_confirm": { + "description": "Pure Energie Meter (`{model}`) \u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Pure Energie Meter device\u3092\u767a\u898b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/nl.json b/homeassistant/components/pure_energie/translations/nl.json new file mode 100644 index 0000000000000..14e705836b088 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Wilt u Pure Energie Meter (`{model}`) toevoegen aan Home Assistant?", + "title": "Ontdekt Pure Energie Meter apparaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/no.json b/homeassistant/components/pure_energie/translations/no.json new file mode 100644 index 0000000000000..6e02b8df2c05f --- /dev/null +++ b/homeassistant/components/pure_energie/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{modell} ({host})", + "step": { + "user": { + "data": { + "host": "Vert" + } + }, + "zeroconf_confirm": { + "description": "Vil du legge til Pure Energie Meter (` {model} `) til Home Assistant?", + "title": "Oppdaget Pure Energie Meter-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/pl.json b/homeassistant/components/pure_energie/translations/pl.json new file mode 100644 index 0000000000000..526326fccc1c7 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/pl.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 Pure Energie Meter (`{model}`) do Home Assistanta?", + "title": "Wykryto urz\u0105dzenie Pure Energie Meter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/pt-BR.json b/homeassistant/components/pure_energie/translations/pt-BR.json new file mode 100644 index 0000000000000..148cd129376f2 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou em conectar" + }, + "error": { + "cannot_connect": "Falhou em conectar" + }, + "flow_title": "{model} ( {host} )", + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Deseja adicionar medidor Pure Energie (` {model} `) ao Home Assistant?", + "title": "Descoberto o dispositivo medidor Pure Energie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/ru.json b/homeassistant/components/pure_energie/translations/ru.json new file mode 100644 index 0000000000000..7673757b245ca --- /dev/null +++ b/homeassistant/components/pure_energie/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Pure Energie Meter (`{model}`)?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Pure Energie Meter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/tr.json b/homeassistant/components/pure_energie/translations/tr.json new file mode 100644 index 0000000000000..8c2a840212483 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Sunucu" + } + }, + "zeroconf_confirm": { + "description": "Home Assistant'a Pure Energie Meter (` {model} `) eklemek istiyor musunuz?", + "title": "Ke\u015ffedilen Pure Energie Meter cihaz\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/zh-Hant.json b/homeassistant/components/pure_energie/translations/zh-Hant.json new file mode 100644 index 0000000000000..7fa7144f91452 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "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": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e Pure Energie Meter (`{model}`) \u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Pure Energie Meter \u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 34356e74a5648..7931cca70ccf5 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pushbullet"] } diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 56bfac0185919..0752fbc7b7851 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pushover_complete"] } diff --git a/homeassistant/components/pvoutput/translations/el.json b/homeassistant/components/pvoutput/translations/el.json index 00c0e1ee3bf76..2c025c4e6d46e 100644 --- a/homeassistant/components/pvoutput/translations/el.json +++ b/homeassistant/components/pvoutput/translations/el.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "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", + "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_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf PVOutput, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {account_url}." }, "user": { "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "system_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" }, "description": "\u0393\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf PVOutput, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {account_url}. \n\n \u03a4\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c4\u03c9\u03bd \u03b5\u03b3\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03c9\u03bd \u03c3\u03c5\u03c3\u03c4\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03c4\u03af\u03b8\u03b5\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03af\u03b4\u03b9\u03b1 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1." diff --git a/homeassistant/components/pvoutput/translations/lv.json b/homeassistant/components/pvoutput/translations/lv.json new file mode 100644 index 0000000000000..eea9a1e042d0f --- /dev/null +++ b/homeassistant/components/pvoutput/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "system_id": "Sist\u0113mas ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/pt-BR.json b/homeassistant/components/pvoutput/translations/pt-BR.json new file mode 100644 index 0000000000000..39bcb9258c665 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "description": "Para re-autenticar com PVOutput, voc\u00ea precisar\u00e1 obter a chave API em {account_url}." + }, + "user": { + "data": { + "api_key": "Chave da API", + "system_id": "ID do sistema" + }, + "description": "Para autenticar com PVOutput, voc\u00ea precisar\u00e1 obter a chave de API em {account_url} . \n\n Os IDs de sistema dos sistemas registrados s\u00e3o listados nessa mesma p\u00e1gina." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/sk.json b/homeassistant/components/pvoutput/translations/sk.json new file mode 100644 index 0000000000000..4eba3bdc8bb9d --- /dev/null +++ b/homeassistant/components/pvoutput/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 5c9c06776b848..7b44d2cfa9562 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aiopvpc==3.0.0"], "codeowners": ["@azogue"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aiopvpc", "holidays"] } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/el.json b/homeassistant/components/pvpc_hourly_pricing/translations/el.json new file mode 100644 index 0000000000000..af128ba3bb31f --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/el.json @@ -0,0 +1,32 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2", + "power": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 (kW)", + "power_p3": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf \u03ba\u03bf\u03b9\u03bb\u03ac\u03b4\u03b1\u03c2 P3 (kW)", + "tariff": "\u0399\u03c3\u03c7\u03cd\u03bf\u03bd \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03b1\u03bd\u03ac \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03ae \u03b6\u03ce\u03bd\u03b7" + }, + "description": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03bf API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03b9 [\u03c9\u03c1\u03b9\u03b1\u03af\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae\u03c2 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 (PVPC)](https://www.esios.ree.es/es/pvpc) \u03c3\u03c4\u03b7\u03bd \u0399\u03c3\u03c0\u03b1\u03bd\u03af\u03b1.\n\u0393\u03b9\u03b1 \u03c0\u03b9\u03bf \u03b1\u03ba\u03c1\u03b9\u03b2\u03b5\u03af\u03c2 \u03b5\u03be\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 (kW)", + "power_p3": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf \u03ba\u03bf\u03b9\u03bb\u03ac\u03b4\u03b1\u03c2 P3 (kW)", + "tariff": "\u0399\u03c3\u03c7\u03cd\u03bf\u03bd \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03b1\u03bd\u03ac \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03ae \u03b6\u03ce\u03bd\u03b7" + }, + "description": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03bf API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03b9 [\u03c9\u03c1\u03b9\u03b1\u03af\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae\u03c2 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 (PVPC)](https://www.esios.ree.es/es/pvpc) \u03c3\u03c4\u03b7\u03bd \u0399\u03c3\u03c0\u03b1\u03bd\u03af\u03b1.\n\u0393\u03b9\u03b1 \u03c0\u03b9\u03bf \u03b1\u03ba\u03c1\u03b9\u03b2\u03b5\u03af\u03c2 \u03b5\u03be\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json index efcaeb801d943..e5754180a7cd6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json @@ -1,16 +1,31 @@ { "config": { "abort": { - "already_configured": "A integra\u00e7\u00e3o j\u00e1 est\u00e1 configurada com um sensor existente com essa tarifa" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { "data": { "name": "Nome do sensor", - "tariff": "Tarifa contratada (1, 2 ou 3 per\u00edodos)" + "power": "Pot\u00eancia contratada (kW)", + "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)", + "tariff": "Tarifa aplic\u00e1vel por zona geogr\u00e1fica" }, - "description": "Esse sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)]](https://www.esios.ree.es/es/pvpc) na Espanha. \nPara uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecione a taxa contratada com base no n\u00famero de per\u00edodos de cobran\u00e7a por dia: \n- 1 per\u00edodo: normal \n- 2 per\u00edodos: discrimina\u00e7\u00e3o (taxa noturna) \n- 3 per\u00edodos: carro el\u00e9trico (taxa noturna de 3 per\u00edodos)", - "title": "Sele\u00e7\u00e3o de tarifas" + "description": "Esse sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)](https://www.esios.ree.es/es/pvpc) na Espanha. \nPara uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecione a taxa contratada com base no n\u00famero de per\u00edodos de cobran\u00e7a por dia: \n- 1 per\u00edodo: normal \n- 2 per\u00edodos: discrimina\u00e7\u00e3o (taxa noturna) \n- 3 per\u00edodos: carro el\u00e9trico (taxa noturna de 3 per\u00edodos)", + "title": "Configura\u00e7\u00e3o do sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Pot\u00eancia contratada (kW)", + "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)", + "tariff": "Tarifa aplic\u00e1vel por zona geogr\u00e1fica" + }, + "description": "Este sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)](https://www.esios.ree.es/es/pvpc) na Espanha.\n Para uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Configura\u00e7\u00e3o do sensor" } } } diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 65c33a41ff324..e4bbf7df3d2e0 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -132,7 +132,7 @@ def update(self): if "speed" in self.type and value > 0: # Convert download rate from Bytes/s to MBytes/s - self._state = round(value / 2 ** 20, 2) + self._state = round(value / 2**20, 2) else: self._state = value diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 8db94bb981747..2bc2763e77766 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/python_script", "requirements": ["restrictedpython==5.2"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "loggers": ["RestrictedPython"] } diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 241b9a5cff9cf..8d49a24a3d9fc 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": ["python-qbittorrent==0.4.2"], "codeowners": ["@geoffreylagaisse"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["qbittorrent"] } diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 5b3de2cf62bf8..366bbdc347983 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": ["georss_qld_bushfire_alert_client==0.5"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["georss_qld_bushfire_alert_client"] } diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index 94f8c8b57886a..15de916201c38 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": ["qnapstats==0.4.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["qnapstats"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 37697f2af830c..259b3ec3b7bc2 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": ["pillow==9.0.1", "pyzbar==0.1.7"], "codeowners": [], - "iot_class": "calculated" + "iot_class": "calculated", + "loggers": ["pyzbar"] } diff --git a/homeassistant/components/qvr_pro/manifest.json b/homeassistant/components/qvr_pro/manifest.json index eb08be180c6c4..70ca1046b9d4d 100644 --- a/homeassistant/components/qvr_pro/manifest.json +++ b/homeassistant/components/qvr_pro/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qvr_pro", "requirements": ["pyqvrpro==0.52"], "codeowners": ["@oblogic7"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyqvrpro"] } diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 851e93dc67d95..eeba565d994ac 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": ["pyqwikswitch==0.93"], "codeowners": ["@kellerza"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyqwikswitch"] } diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 735e2f35bf4aa..4ce203b24999f 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -30,5 +30,6 @@ "name": "rachio*" } ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["rachiopy"] } diff --git a/homeassistant/components/rachio/translations/el.json b/homeassistant/components/rachio/translations/el.json new file mode 100644 index 0000000000000..3e3a14f04075d --- /dev/null +++ b/homeassistant/components/rachio/translations/el.json @@ -0,0 +1,30 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b1\u03c0\u03cc \u03c4\u03bf https://app.rach.io/. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf 'GET API KEY'.", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/pt-BR.json b/homeassistant/components/rachio/translations/pt-BR.json new file mode 100644 index 0000000000000..c1c53e065dced --- /dev/null +++ b/homeassistant/components/rachio/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + }, + "description": "Voc\u00ea precisar\u00e1 da chave de API de https://app.rach.io/. V\u00e1 para Configura\u00e7\u00f5es e clique em 'GET API KEY'.", + "title": "Conecte-se ao seu dispositivo Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Dura\u00e7\u00e3o em minutos para ser executada ao ativar um interruptor de zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/sk.json b/homeassistant/components/rachio/translations/sk.json new file mode 100644 index 0000000000000..ff85312780312 --- /dev/null +++ b/homeassistant/components/rachio/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 0244318035818..d190a2e96eac6 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -138,10 +138,16 @@ def setup_platform( ) -> None: """Set up the Radarr platform.""" conditions = config[CONF_MONITORED_CONDITIONS] + # deprecated in 2022.3 + if "wanted" in conditions: + _LOGGER.warning( + "Wanted is not a valid condition option. Please remove it from your config" + ) entities = [ RadarrSensor(hass, config, description) for description in SENSOR_TYPES if description.key in conditions + if description.key != "wanted" ] add_entities(entities, True) diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py new file mode 100644 index 0000000000000..89c2f22015941 --- /dev/null +++ b/homeassistant/components/radio_browser/__init__.py @@ -0,0 +1,36 @@ +"""The Radio Browser integration.""" +from __future__ import annotations + +from radios import RadioBrowser, RadioBrowserError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Radio Browser from a config entry. + + This integration doesn't set up any enitites, as it provides a media source + only. + """ + session = async_get_clientsession(hass) + radios = RadioBrowser(session=session, user_agent=f"HomeAssistant/{__version__}") + + try: + await radios.stats() + except RadioBrowserError as err: + raise ConfigEntryNotReady("Could not connect to Radio Browser API") from err + + hass.data[DOMAIN] = radios + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + del hass.data[DOMAIN] + return True diff --git a/homeassistant/components/radio_browser/config_flow.py b/homeassistant/components/radio_browser/config_flow.py new file mode 100644 index 0000000000000..1c6964d0715da --- /dev/null +++ b/homeassistant/components/radio_browser/config_flow.py @@ -0,0 +1,33 @@ +"""Config flow for Radio Browser integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class RadioBrowserConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Radio Browser.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title="Radio Browser", data={}) + + return self.async_show_form(step_id="user") + + async def async_step_onboarding( + self, data: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by onboarding.""" + return self.async_create_entry(title="Radio Browser", data={}) diff --git a/homeassistant/components/radio_browser/const.py b/homeassistant/components/radio_browser/const.py new file mode 100644 index 0000000000000..eb456db08e843 --- /dev/null +++ b/homeassistant/components/radio_browser/const.py @@ -0,0 +1,7 @@ +"""Constants for the Radio Browser integration.""" +import logging +from typing import Final + +DOMAIN: Final = "radio_browser" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json new file mode 100644 index 0000000000000..865d8b25ab1e7 --- /dev/null +++ b/homeassistant/components/radio_browser/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "radio_browser", + "name": "Radio Browser", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/radio", + "requirements": ["radios==0.1.0"], + "codeowners": ["@frenck"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py new file mode 100644 index 0000000000000..6ba1b7b2b9aef --- /dev/null +++ b/homeassistant/components/radio_browser/media_source.py @@ -0,0 +1,302 @@ +"""Expose Radio Browser as a media source.""" +from __future__ import annotations + +import mimetypes + +from radios import FilterBy, Order, RadioBrowser, Station + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, + MEDIA_TYPE_MUSIC, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + +CODEC_TO_MIMETYPE = { + "MP3": "audio/mpeg", + "AAC": "audio/aac", + "AAC+": "audio/aac", + "OGG": "application/ogg", +} + + +async def async_get_media_source(hass: HomeAssistant) -> RadioMediaSource: + """Set up Radio Browser media source.""" + # Radio browser support only a single config entry + entry = hass.config_entries.async_entries(DOMAIN)[0] + + return RadioMediaSource(hass, entry) + + +class RadioMediaSource(MediaSource): + """Provide Radio stations as media sources.""" + + name = "Radio Browser" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize CameraMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + self.entry = entry + + @property + def radios(self) -> RadioBrowser | None: + """Return the radio browser.""" + return self.hass.data.get(DOMAIN) + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve selected Radio station to a streaming URL.""" + radios = self.radios + + if radios is None: + raise Unresolvable("Radio Browser not initialized") + + station = await radios.station(uuid=item.identifier) + if not station: + raise Unresolvable("Radio station is no longer available") + + if not (mime_type := self._async_get_station_mime_type(station)): + raise Unresolvable("Could not determine stream type of radio station") + + # Register "click" with Radio Browser + await radios.station_click(uuid=station.uuid) + + return PlayMedia(station.url, mime_type) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + radios = self.radios + + if radios is None: + raise BrowseError("Radio Browser not initialized") + + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MEDIA_TYPE_MUSIC, + title=self.entry.title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + children=[ + *await self._async_build_popular(radios, item), + *await self._async_build_by_tag(radios, item), + *await self._async_build_by_language(radios, item), + *await self._async_build_by_country(radios, item), + ], + ) + + @callback + @staticmethod + def _async_get_station_mime_type(station: Station) -> str | None: + """Determine mime type of a radio station.""" + mime_type = CODEC_TO_MIMETYPE.get(station.codec) + if not mime_type: + mime_type, _ = mimetypes.guess_type(station.url) + return mime_type + + @callback + def _async_build_stations( + self, radios: RadioBrowser, stations: list[Station] + ) -> list[BrowseMediaSource]: + """Build list of media sources from radio stations.""" + items: list[BrowseMediaSource] = [] + + for station in stations: + if station.codec == "UNKNOWN" or not ( + mime_type := self._async_get_station_mime_type(station) + ): + continue + + items.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=station.uuid, + media_class=MEDIA_CLASS_MUSIC, + media_content_type=mime_type, + title=station.name, + can_play=True, + can_expand=False, + thumbnail=station.favicon, + ) + ) + + return items + + async def _async_build_by_country( + self, radios: RadioBrowser, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing radio stations by country.""" + category, _, country_code = (item.identifier or "").partition("/") + if country_code: + stations = await radios.stations( + filter_by=FilterBy.COUNTRY_CODE_EXACT, + filter_term=country_code, + hide_broken=True, + order=Order.NAME, + reverse=False, + ) + return self._async_build_stations(radios, stations) + + # We show country in the root additionally, when there is no item + if not item.identifier or category == "country": + countries = await radios.countries(order=Order.NAME) + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"country/{country.code}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title=country.name, + can_play=False, + can_expand=True, + thumbnail=country.favicon, + ) + for country in countries + ] + + return [] + + async def _async_build_by_language( + self, radios: RadioBrowser, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing radio stations by language.""" + category, _, language = (item.identifier or "").partition("/") + if category == "language" and language: + stations = await radios.stations( + filter_by=FilterBy.LANGUAGE_EXACT, + filter_term=language, + hide_broken=True, + order=Order.NAME, + reverse=False, + ) + return self._async_build_stations(radios, stations) + + if category == "language": + languages = await radios.languages(order=Order.NAME, hide_broken=True) + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"language/{language.code}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title=language.name, + can_play=False, + can_expand=True, + thumbnail=language.favicon, + ) + for language in languages + ] + + if not item.identifier: + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier="language", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title="By Language", + can_play=False, + can_expand=True, + ) + ] + + return [] + + async def _async_build_popular( + self, radios: RadioBrowser, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing popular radio stations.""" + if item.identifier == "popular": + stations = await radios.stations( + hide_broken=True, + limit=250, + order=Order.CLICK_COUNT, + reverse=True, + ) + return self._async_build_stations(radios, stations) + + if not item.identifier: + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier="popular", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title="Popular", + can_play=False, + can_expand=True, + ) + ] + + return [] + + async def _async_build_by_tag( + self, radios: RadioBrowser, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing radio stations by tags.""" + category, _, tag = (item.identifier or "").partition("/") + if category == "tag" and tag: + stations = await radios.stations( + filter_by=FilterBy.TAG_EXACT, + filter_term=tag, + hide_broken=True, + order=Order.NAME, + reverse=False, + ) + return self._async_build_stations(radios, stations) + + if category == "tag": + tags = await radios.tags( + hide_broken=True, + limit=100, + order=Order.STATION_COUNT, + reverse=True, + ) + + # Now we have the top tags, reorder them by name + tags.sort(key=lambda tag: tag.name) + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"tag/{tag.name}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title=tag.name.title(), + can_play=False, + can_expand=True, + ) + for tag in tags + ] + + if not item.identifier: + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier="tag", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_MUSIC, + title="By Category", + can_play=False, + can_expand=True, + ) + ] + + return [] diff --git a/homeassistant/components/radio_browser/strings.json b/homeassistant/components/radio_browser/strings.json new file mode 100644 index 0000000000000..7bf9bc9ca66f7 --- /dev/null +++ b/homeassistant/components/radio_browser/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to add Radio Browser to Home Assistant?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/ca.json b/homeassistant/components/radio_browser/translations/ca.json new file mode 100644 index 0000000000000..50b6e62d751a2 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols afegir el navegador r\u00e0dio a Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/de.json b/homeassistant/components/radio_browser/translations/de.json new file mode 100644 index 0000000000000..094e66dd3f5a1 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du den Radio-Browser zu Home Assistant hinzuf\u00fcgen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/el.json b/homeassistant/components/radio_browser/translations/el.json new file mode 100644 index 0000000000000..4c847a0fb6861 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Radio Browser \u03c3\u03c4\u03bf Home Assistant;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/en.json b/homeassistant/components/radio_browser/translations/en.json new file mode 100644 index 0000000000000..5f89dd9447c89 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to add Radio Browser to Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/et.json b/homeassistant/components/radio_browser/translations/et.json new file mode 100644 index 0000000000000..5c5d742654ceb --- /dev/null +++ b/homeassistant/components/radio_browser/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "user": { + "description": "Kas lisada Home Assistantile Radio Browser?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/hu.json b/homeassistant/components/radio_browser/translations/hu.json new file mode 100644 index 0000000000000..fbc52f3b1de79 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 hozz\u00e1adni Home Assistanthoz: Radio Browser?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/it.json b/homeassistant/components/radio_browser/translations/it.json new file mode 100644 index 0000000000000..761aa3467a576 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/it.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi aggiungere Radio Browser a Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/ja.json b/homeassistant/components/radio_browser/translations/ja.json new file mode 100644 index 0000000000000..24b32e6e30a9f --- /dev/null +++ b/homeassistant/components/radio_browser/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "Home Assistant\u306b\u3001Radio Browser\u3092\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/nl.json b/homeassistant/components/radio_browser/translations/nl.json new file mode 100644 index 0000000000000..d8f46a3130b1c --- /dev/null +++ b/homeassistant/components/radio_browser/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u Radio Browser toevoegen aan Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/no.json b/homeassistant/components/radio_browser/translations/no.json new file mode 100644 index 0000000000000..8646b43508eb7 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du legge til Radio Browser til Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/pl.json b/homeassistant/components/radio_browser/translations/pl.json new file mode 100644 index 0000000000000..903848b73eadb --- /dev/null +++ b/homeassistant/components/radio_browser/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz doda\u0107 radia internetowe do Home Assistanta?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/pt-BR.json b/homeassistant/components/radio_browser/translations/pt-BR.json new file mode 100644 index 0000000000000..b25a8cbef92b7 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja adicionar o Radio Browser ao Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/ru.json b/homeassistant/components/radio_browser/translations/ru.json new file mode 100644 index 0000000000000..f97f10c1efb1e --- /dev/null +++ b/homeassistant/components/radio_browser/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Radio Browser?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/zh-Hant.json b/homeassistant/components/radio_browser/translations/zh-Hant.json new file mode 100644 index 0000000000000..a826b3311938d --- /dev/null +++ b/homeassistant/components/radio_browser/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u5ee3\u64ad\u700f\u89bd\u5668\u65b0\u589e\u81f3 Home Assistant\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index b051ba65b3b0c..72c2c8eb3004f 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], "codeowners": ["@vinnyfuria"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["radiotherm"] } diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index d7d3c064ad754..47bb7ce9bd9a6 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": ["pyrainbird==0.4.3"], "codeowners": ["@konikvranik"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyrainbird"] } diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index 309dc6bdb5199..ac049f00316cb 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": ["raincloudy==0.0.7"], "codeowners": ["@vanstinator"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["raincloudy"] } diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 10a7dc35ddcf5..b4fbc78f24154 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -10,5 +10,6 @@ { "macaddress": "D8D5B9*" } - ] + ], + "loggers": ["aioeagle", "uEagle"] } diff --git a/homeassistant/components/rainforest_eagle/translations/el.json b/homeassistant/components/rainforest_eagle/translations/el.json index 686a0d72c440e..fcf9a27cd1bb4 100644 --- a/homeassistant/components/rainforest_eagle/translations/el.json +++ b/homeassistant/components/rainforest_eagle/translations/el.json @@ -4,12 +4,15 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7" }, "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\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { "data": { "cloud_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bd\u03ad\u03c6\u03bf\u03c5\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "install_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2" } } diff --git a/homeassistant/components/rainforest_eagle/translations/pt-BR.json b/homeassistant/components/rainforest_eagle/translations/pt-BR.json new file mode 100644 index 0000000000000..e40f41a615289 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "cloud_id": "Cloud ID", + "host": "Nome do host", + "install_code": "C\u00f3digo de instala\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/sk.json b/homeassistant/components/rainforest_eagle/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4a272ea036442..331f191d029cb 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -14,5 +14,6 @@ "type": "_http._tcp.local.", "name": "rainmachine*" } - ] + ], + "loggers": ["regenmaschine"] } diff --git a/homeassistant/components/rainmachine/translations/el.json b/homeassistant/components/rainmachine/translations/el.json index 8c2e276df85e6..a244cc58ab391 100644 --- a/homeassistant/components/rainmachine/translations/el.json +++ b/homeassistant/components/rainmachine/translations/el.json @@ -1,10 +1,31 @@ { "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": { + "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" + }, "flow_title": "{ip}", "step": { "user": { + "data": { + "ip_address": "\u038c\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", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" } } + }, + "options": { + "step": { + "init": { + "data": { + "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" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/pt-BR.json b/homeassistant/components/rainmachine/translations/pt-BR.json index e876d3675759f..6359b1b6ae9e1 100644 --- a/homeassistant/components/rainmachine/translations/pt-BR.json +++ b/homeassistant/components/rainmachine/translations/pt-BR.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{ip}", "step": { "user": { "data": { @@ -10,5 +17,15 @@ "title": "Preencha suas informa\u00e7\u00f5es" } } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "Tempo de execu\u00e7\u00e3o da zona padr\u00e3o (em segundos)" + }, + "title": "Configurar RainMachine" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/sk.json b/homeassistant/components/rainmachine/translations/sk.json new file mode 100644 index 0000000000000..7fd0d4942e853 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index 6fd4b13dee07b..56f4855d460c9 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": ["raspyrfm-client==1.2.8"], "codeowners": [], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["raspyrfm_client"] } diff --git a/homeassistant/components/rdw/translations/el.json b/homeassistant/components/rdw/translations/el.json new file mode 100644 index 0000000000000..607a99c87bf7c --- /dev/null +++ b/homeassistant/components/rdw/translations/el.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown_license_plate": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7 \u03c0\u03b9\u03bd\u03b1\u03ba\u03af\u03b4\u03b1 \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1\u03c2" + }, + "step": { + "user": { + "data": { + "license_plate": "\u03a0\u03b9\u03bd\u03b1\u03ba\u03af\u03b4\u03b1 \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/translations/pt-BR.json b/homeassistant/components/rdw/translations/pt-BR.json new file mode 100644 index 0000000000000..57de88fd6e9d9 --- /dev/null +++ b/homeassistant/components/rdw/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "unknown_license_plate": "Placa desconhecida" + }, + "step": { + "user": { + "data": { + "license_plate": "Placa de carro" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 85cb7100a6507..68fc0e2c30926 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": ["aiorecollect==1.0.8"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aiorecollect"] } diff --git a/homeassistant/components/recollect_waste/translations/el.json b/homeassistant/components/recollect_waste/translations/el.json index 5dbfa18b18c01..fa4af8002485a 100644 --- a/homeassistant/components/recollect_waste/translations/el.json +++ b/homeassistant/components/recollect_waste/translations/el.json @@ -1,5 +1,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" + }, "error": { "invalid_place_or_service_id": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b8\u03ad\u03c3\u03b7\u03c2 \u03ae \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1\u03c2" }, @@ -11,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c6\u03b9\u03bb\u03b9\u03ba\u03ac \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03cd\u03c0\u03bf\u03c5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae\u03c2 (\u03cc\u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03bd)" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pt-BR.json b/homeassistant/components/recollect_waste/translations/pt-BR.json new file mode 100644 index 0000000000000..0df3b63a2f814 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_place_or_service_id": "ID de local ou ID de servi\u00e7o inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "place_id": "ID do lugar", + "service_id": "ID de servi\u00e7o" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Use nomes amig\u00e1veis para tipos de coleta (quando poss\u00edvel)" + }, + "title": "Configurar Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 55d6f73108c99..579f47ed4a7d1 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -77,7 +77,7 @@ ) -class Events(Base): # type: ignore +class Events(Base): # type: ignore[misc,valid-type] """Event history data.""" __table_args__ = ( @@ -141,7 +141,7 @@ def to_native(self, validate_entity_id=True): return None -class States(Base): # type: ignore +class States(Base): # type: ignore[misc,valid-type] """State change history.""" __table_args__ = ( @@ -276,13 +276,13 @@ def metadata_id(self): @classmethod def from_stats(cls, metadata_id: int, stats: StatisticData): """Create object from a statistics.""" - return cls( # type: ignore + return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, **stats, ) -class Statistics(Base, StatisticsBase): # type: ignore +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] """Long term statistics.""" duration = timedelta(hours=1) @@ -294,7 +294,7 @@ class Statistics(Base, StatisticsBase): # type: ignore __tablename__ = TABLE_STATISTICS -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] """Short term statistics.""" duration = timedelta(minutes=5) @@ -322,7 +322,7 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore +class StatisticsMeta(Base): # type: ignore[misc,valid-type] """Statistics meta data.""" __table_args__ = ( @@ -343,7 +343,7 @@ def from_meta(meta: StatisticMetaData) -> StatisticsMeta: return StatisticsMeta(**meta) -class RecorderRuns(Base): # type: ignore +class RecorderRuns(Base): # type: ignore[misc,valid-type] """Representation of recorder run.""" __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -393,7 +393,7 @@ def to_native(self, validate_entity_id=True): return self -class SchemaChanges(Base): # type: ignore +class SchemaChanges(Base): # type: ignore[misc,valid-type] """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES @@ -411,7 +411,7 @@ def __repr__(self) -> str: ) -class StatisticsRuns(Base): # type: ignore +class StatisticsRuns(Base): # type: ignore[misc,valid-type] """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS @@ -491,7 +491,7 @@ def __init__(self, row): # pylint: disable=super-init-not-called self._last_updated = None self._context = None - @property # type: ignore + @property # type: ignore[override] def attributes(self): """State attributes.""" if not self._attributes: @@ -508,7 +508,7 @@ def attributes(self, value): """Set attributes.""" self._attributes = value - @property # type: ignore + @property # type: ignore[override] def context(self): """State context.""" if not self._context: @@ -520,7 +520,7 @@ def context(self, value): """Set context.""" self._context = value - @property # type: ignore + @property # type: ignore[override] def last_changed(self): """Last changed datetime.""" if not self._last_changed: @@ -532,7 +532,7 @@ def last_changed(self, value): """Set last changed datetime.""" self._last_changed = value - @property # type: ignore + @property # type: ignore[override] def last_updated(self): """Last updated datetime.""" if not self._last_updated: diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 9ee89d248cced..b30237f98da94 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -5,7 +5,7 @@ class RecorderPool(StaticPool, NullPool): - """A hybird of NullPool and StaticPool. + """A hybrid of NullPool and StaticPool. When called from the creating thread acts like StaticPool When called from any other thread, acts like NullPool diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index e44ae9aafff3e..dd80fb15479d5 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -34,7 +34,7 @@ def purge_old_data( purge_before.isoformat(sep=" ", timespec="seconds"), ) - with session_scope(session=instance.get_session()) as session: # type: ignore + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record event_ids = _select_event_ids_to_purge(session, purge_before) state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) @@ -267,7 +267,7 @@ def _purge_filtered_states( "Selected %s state_ids to remove that should be filtered", len(state_ids) ) _purge_state_ids(instance, session, set(state_ids)) - _purge_event_ids(session, event_ids) # type: ignore # type of event_ids already narrowed to 'list[int]' + _purge_event_ids(session, event_ids) # type: ignore[arg-type] # type of event_ids already narrowed to 'list[int]' def _purge_filtered_events( @@ -295,7 +295,7 @@ def _purge_filtered_events( @retryable_database_job("purge") def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: """Purge states and events of specified entities.""" - with session_scope(session=instance.get_session()) as session: # type: ignore + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] selected_entity_ids: list[str] = [ entity_id for (entity_id,) in session.query(distinct(States.entity_id)).all() diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index b2ea33fe2dd78..43ff7548dd6d1 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -44,7 +44,7 @@ purge_entities: entity_globs: name: Entity Globs to remove - description: List the regular expressions to select entities for removal from the recorder database. + description: List the glob patterns to select entities for removal from the recorder database. example: "domain*.object_id*" required: false default: [] diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 6c305242f5fd0..4154ae830555f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -488,7 +488,7 @@ def compile_hourly_statistics( ) if stats: - for metadata_id, group in groupby(stats, lambda stat: stat["metadata_id"]): # type: ignore + for metadata_id, group in groupby(stats, lambda stat: stat["metadata_id"]): # type: ignore[no-any-return] ( metadata_id, last_reset, @@ -527,7 +527,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: end = start + timedelta(minutes=5) # Return if we already have 5-minute statistics for the requested period - with session_scope(session=instance.get_session()) as session: # type: ignore + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] if session.query(StatisticsRuns).filter_by(start=start).first(): _LOGGER.debug("Statistics already compiled for %s-%s", start, end) return True @@ -546,7 +546,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: # Insert collected statistics in the database with session_scope( - session=instance.get_session(), # type: ignore + session=instance.get_session(), # type: ignore[misc] exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: for stats in platform_stats: @@ -700,7 +700,7 @@ def _configured_unit(unit: str, units: UnitSystem) -> str: 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: # type: ignore + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] session.query(StatisticsMeta).filter( StatisticsMeta.statistic_id.in_(statistic_ids) ).delete(synchronize_session=False) @@ -710,7 +710,7 @@ def update_statistics_metadata( instance: Recorder, statistic_id: str, unit_of_measurement: str | None ) -> None: """Update statistics metadata for a statistic_id.""" - with session_scope(session=instance.get_session()) as session: # type: ignore + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] session.query(StatisticsMeta).filter( StatisticsMeta.statistic_id == statistic_id ).update({StatisticsMeta.unit_of_measurement: unit_of_measurement}) @@ -1093,7 +1093,7 @@ def _sorted_statistics_to_dict( def no_conversion(val: Any, _: Any) -> float | None: """Return x.""" - return val # type: ignore + return val # type: ignore[no-any-return] # Set all statistic IDs to empty lists in result set to maintain the order if statistic_ids is not None: @@ -1101,7 +1101,7 @@ def no_conversion(val: Any, _: Any) -> float | None: result[stat_id] = [] # Identify metadata IDs for which no data was available at the requested start time - for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore + for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] first_start_time = process_timestamp(next(group).start) if start_time and first_start_time > start_time: need_stat_at_start_time.add(meta_id) @@ -1115,12 +1115,12 @@ def no_conversion(val: Any, _: Any) -> float | None: stats_at_start_time[stat.metadata_id] = (stat,) # Append all statistic entries, and optionally do unit conversion - for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore + for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore[no-any-return] unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] convert: Callable[[Any, Any], float | None] if convert_units: - convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore + convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] else: convert = no_conversion ent_results = result[meta_id] @@ -1249,7 +1249,7 @@ def add_external_statistics( """Process an add_statistics job.""" with session_scope( - session=instance.get_session(), # type: ignore + session=instance.get_session(), # type: ignore[misc] exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: metadata_id = _update_or_add_metadata(instance.hass, session, metadata) diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index c8a724471883e..dfe177b05a8c5 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": ["pyrecswitch==1.0.2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyrecswitch"] } diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 631414ad34434..f641bfd7a5788 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": ["praw==7.4.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["praw", "prawcore"] } diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 58594f1757745..93f359b4f78c1 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": ["rjpl==0.3.6"], "codeowners": ["@DarkFox"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["rjpl"] } diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index c19cc701afce5..40bfbe1683c21 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -5,5 +5,6 @@ "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], "dependencies": ["configurator"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["rtmapi"] } diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3130484d10b57..bdeef15971ec1 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -47,7 +47,7 @@ send_command: required: true example: "Play" selector: - text: + object: num_repeats: name: Repeats description: The number of times you want to repeat the command(s). diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index f14e786aab661..dfa90cb1cc810 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { + "changed_states": "{entity_name} activado o desactivado", "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index 2012c853dddbc..c2052edaab870 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { + "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} s'est \u00e9teint", "turned_on": "{entity_name} s'est allum\u00e9" diff --git a/homeassistant/components/remote/translations/id.json b/homeassistant/components/remote/translations/id.json index 09552be40d49d..34eaa019be237 100644 --- a/homeassistant/components/remote/translations/id.json +++ b/homeassistant/components/remote/translations/id.json @@ -10,6 +10,8 @@ "is_on": "{entity_name} nyala" }, "trigger_type": { + "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", + "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json index 18d984f5c68f5..47ba3d7eda740 100644 --- a/homeassistant/components/remote/translations/nl.json +++ b/homeassistant/components/remote/translations/nl.json @@ -10,6 +10,8 @@ "is_on": "{entity_name} staat aan" }, "trigger_type": { + "changed_states": "{entity_name} in- of uitgeschakeld", + "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" } diff --git a/homeassistant/components/remote/translations/pl.json b/homeassistant/components/remote/translations/pl.json index 2724fdd05f247..2aaaf6dbd0124 100644 --- a/homeassistant/components/remote/translations/pl.json +++ b/homeassistant/components/remote/translations/pl.json @@ -10,6 +10,7 @@ "is_on": "pilot {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { + "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" diff --git a/homeassistant/components/remote/translations/pt-BR.json b/homeassistant/components/remote/translations/pt-BR.json index d658a07f4df45..15d9b0d7c76e0 100644 --- a/homeassistant/components/remote/translations/pt-BR.json +++ b/homeassistant/components/remote/translations/pt-BR.json @@ -1,4 +1,21 @@ { + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "changed_states": "{entity_name} ligado ou desligado", + "toggled": "{entity_name} ligado ou desligado", + "turned_off": "{entity_name} for desligado", + "turned_on": "{entity_name} for ligado" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index b2ed060bffaba..7e42611dedf0a 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": ["gpiozero==1.5.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["gpiozero"] } diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index c2ebdb5cb0f50..a24c9be4e6d58 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Renault binary sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from renault_api.kamereon.enums import ChargeState, PlugState @@ -37,6 +38,8 @@ class RenaultBinarySensorEntityDescription( ): """Class describing Renault binary sensor entities.""" + icon_fn: Callable[[RenaultBinarySensor], str] | None = None + async def async_setup_entry( hass: HomeAssistant, @@ -64,27 +67,73 @@ class RenaultBinarySensor( @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" - return ( - self._get_data_attr(self.entity_description.on_key) - == self.entity_description.on_value - ) - + if (data := self._get_data_attr(self.entity_description.on_key)) is None: + return None + return data == self.entity_description.on_value -BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = ( - RenaultBinarySensorEntityDescription( - key="plugged_in", - coordinator="battery", - device_class=BinarySensorDeviceClass.PLUG, - name="Plugged In", - on_key="plugStatus", - on_value=PlugState.PLUGGED.value, - ), - RenaultBinarySensorEntityDescription( - key="charging", - coordinator="battery", - device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - name="Charging", - on_key="chargingStatus", - on_value=ChargeState.CHARGE_IN_PROGRESS.value, - ), + @property + def icon(self) -> str | None: + """Icon handling.""" + if self.entity_description.icon_fn: + return self.entity_description.icon_fn(self) + return None + + +BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( + [ + RenaultBinarySensorEntityDescription( + key="plugged_in", + coordinator="battery", + device_class=BinarySensorDeviceClass.PLUG, + name="Plugged In", + on_key="plugStatus", + on_value=PlugState.PLUGGED.value, + ), + RenaultBinarySensorEntityDescription( + key="charging", + coordinator="battery", + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + name="Charging", + on_key="chargingStatus", + on_value=ChargeState.CHARGE_IN_PROGRESS.value, + ), + RenaultBinarySensorEntityDescription( + key="hvac_status", + coordinator="hvac_status", + icon_fn=lambda e: "mdi:fan" if e.is_on else "mdi:fan-off", + name="HVAC", + on_key="hvacStatus", + on_value="on", + ), + RenaultBinarySensorEntityDescription( + key="lock_status", + coordinator="lock_status", + # lock: on means open (unlocked), off means closed (locked) + device_class=BinarySensorDeviceClass.LOCK, + name="Lock", + on_key="lockStatus", + on_value="unlocked", + ), + RenaultBinarySensorEntityDescription( + key="hatch_status", + coordinator="lock_status", + # On means open, Off means closed + device_class=BinarySensorDeviceClass.DOOR, + name="Hatch", + on_key="hatchStatus", + on_value="open", + ), + ] + + [ + RenaultBinarySensorEntityDescription( + key=f"{door.replace(' ','_').lower()}_door_status", + coordinator="lock_status", + # On means open, Off means closed + device_class=BinarySensorDeviceClass.DOOR, + name=f"{door} Door", + on_key=f"doorStatus{door.replace(' ','')}", + on_value="open", + ) + for door in ("Rear Left", "Rear Right", "Driver", "Passenger") + ], ) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 0b0c7d98164c7..71e2e7d64b8a5 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -4,10 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", "requirements": [ - "renault-api==0.1.8" + "renault-api==0.1.9" ], "codeowners": [ "@epenet" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["renault_api"], + "supported_brands":{"dacia":"Dacia"} } diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 462c5bbc2397d..12860bc6b9aeb 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -148,4 +148,14 @@ async def async_initialise(self) -> None: requires_electricity=True, update_method=lambda x: x.get_charge_mode, ), + RenaultCoordinatorDescription( + endpoint="lock-status", + key="lock_status", + update_method=lambda x: x.get_lock_status, + ), + RenaultCoordinatorDescription( + endpoint="res-state", + key="res_state", + update_method=lambda x: x.get_res_state, + ), ) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index d98b986487545..c6621b16bbc4d 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -12,6 +12,7 @@ KamereonVehicleCockpitData, KamereonVehicleHvacStatusData, KamereonVehicleLocationData, + KamereonVehicleResStateData, ) from homeassistant.components.sensor import ( @@ -305,6 +306,24 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), + RenaultSensorEntityDescription( + key="hvac_soc_threshold", + coordinator="hvac_status", + data_key="socThreshold", + entity_class=RenaultSensor[KamereonVehicleHvacStatusData], + name="HVAC SOC Threshold", + native_unit_of_measurement=PERCENTAGE, + ), + RenaultSensorEntityDescription( + key="hvac_last_activity", + coordinator="hvac_status", + device_class=SensorDeviceClass.TIMESTAMP, + data_key="lastUpdateTime", + entity_class=RenaultSensor[KamereonVehicleHvacStatusData], + entity_registry_enabled_default=False, + name="HVAC Last Activity", + value_lambda=_get_utc_value, + ), RenaultSensorEntityDescription( key="location_last_activity", coordinator="location", @@ -315,4 +334,19 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: name="Location Last Activity", value_lambda=_get_utc_value, ), + RenaultSensorEntityDescription( + key="res_state", + coordinator="res_state", + data_key="details", + entity_class=RenaultSensor[KamereonVehicleResStateData], + name="Remote Engine Start", + ), + RenaultSensorEntityDescription( + key="res_state_code", + coordinator="res_state", + data_key="code", + entity_class=RenaultSensor[KamereonVehicleResStateData], + entity_registry_enabled_default=False, + name="Remote Engine Start Code", + ), ) diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index de69daefef6b5..91dc31d17f7e7 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -1,9 +1,9 @@ """Support for Renault services.""" from __future__ import annotations +from collections.abc import Mapping from datetime import datetime import logging -from types import MappingProxyType from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -126,7 +126,7 @@ async def charge_start(service_call: ServiceCall) -> None: result = await proxy.vehicle.set_charge_start() LOGGER.debug("Charge start result: %s", result) - def get_vehicle_proxy(service_call_data: MappingProxyType) -> RenaultVehicleProxy: + def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy: """Get vehicle from service_call data.""" device_registry = dr.async_get(hass) device_id = service_call_data[ATTR_VEHICLE] diff --git a/homeassistant/components/renault/translations/el.json b/homeassistant/components/renault/translations/el.json index 4f29e85686594..1d8c0e543fed6 100644 --- a/homeassistant/components/renault/translations/el.json +++ b/homeassistant/components/renault/translations/el.json @@ -1,8 +1,34 @@ { "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", + "kamereon_no_account": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Kamereon", + "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_credentials": "\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": { + "kamereon": { + "data": { + "kamereon_account_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Kamereon" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Kamereon" + }, "reauth_confirm": { - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\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 \u03c4\u03bf {username}" + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\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 \u03c4\u03bf {username}", + "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": { + "locale": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd Renault" } } } diff --git a/homeassistant/components/renault/translations/pt-BR.json b/homeassistant/components/renault/translations/pt-BR.json new file mode 100644 index 0000000000000..28054eac1c588 --- /dev/null +++ b/homeassistant/components/renault/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "kamereon_no_account": "N\u00e3o foi poss\u00edvel encontrar a conta Kamereon", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "kamereon": { + "data": { + "kamereon_account_id": "ID da conta Kamereon" + }, + "title": "Selecione o ID da conta Kamereon" + }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Atualize sua senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "locale": "Localidade", + "password": "Senha", + "username": "Email" + }, + "title": "Definir credenciais Renault" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/sk.json b/homeassistant/components/renault/translations/sk.json new file mode 100644 index 0000000000000..d1d6ad7289806 --- /dev/null +++ b/homeassistant/components/renault/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_credentials": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 463c42c3a6467..8f7ffc2766a38 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": ["pyrepetierng==0.1.0"], "codeowners": ["@MTrab", "@ShadowBr0ther"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyrepetierng"] } diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 9b6b7c6b6c60f..ce723e84b8cc3 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -13,11 +13,13 @@ CONF_DEVICES, CONF_FORCE_UPDATE, CONF_NAME, + STATE_ON, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import CONF_ALIASES, RflinkDevice @@ -67,7 +69,7 @@ async def async_setup_platform( async_add_entities(devices_from_config(config)) -class RflinkBinarySensor(RflinkDevice, BinarySensorEntity): +class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): """Representation of an Rflink binary sensor.""" def __init__( @@ -81,6 +83,15 @@ def __init__( self._delay_listener = None super().__init__(device_id, **kwargs) + async def async_added_to_hass(self): + """Restore RFLink BinarySensor state.""" + await super().async_added_to_hass() + if (old_state := await self.async_get_last_state()) is not None: + if self._off_delay is None: + self._state = old_state.state == STATE_ON + else: + self._state = False + def _handle_event(self, event): """Domain specific event handler.""" command = event["command"] diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index b14f7594d7139..debc12ae4e000 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": ["rflink==0.0.62"], "codeowners": ["@javicalle"], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["rflink"] } diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 6700e66a94124..8dda4d32644f0 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -5,9 +5,8 @@ import binascii from collections.abc import Callable import copy -import functools import logging -from typing import NamedTuple +from typing import NamedTuple, cast import RFXtrx as rfxtrxmod import async_timeout @@ -25,9 +24,13 @@ EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntry, + DeviceRegistry, +) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -37,7 +40,6 @@ COMMAND_GROUP_LIST, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, - CONF_REMOVE_DEVICE, DATA_RFXOBJECT, DEVICE_PACKET_TYPE_LIGHTING4, EVENT_RFXTRX_EVENT, @@ -82,7 +84,9 @@ def _bytearray_string(data): ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: """Set up the RFXtrx component.""" hass.data.setdefault(DOMAIN, {}) @@ -224,6 +228,34 @@ def _add_device(event, device_id): hass.config_entries.async_update_entry(entry=entry, data=data) devices[device_id] = config + @callback + def _remove_device(device_id: DeviceTuple): + data = { + **entry.data, + CONF_DEVICES: { + packet_id: entity_info + for packet_id, entity_info in entry.data[CONF_DEVICES].items() + if tuple(entity_info.get(CONF_DEVICE_ID)) != device_id + }, + } + hass.config_entries.async_update_entry(entry=entry, data=data) + devices.pop(device_id) + + @callback + def _updated_device(event: Event): + if event.data["action"] != "remove": + return + device_entry = device_registry.deleted_devices[event.data["device_id"]] + if entry.entry_id not in device_entry.config_entries: + return + device_id = get_device_tuple_from_identifiers(device_entry.identifiers) + if device_id: + _remove_device(device_id) + + entry.async_on_unload( + hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) + ) + def _shutdown_rfxtrx(event): """Close connection with RFXtrx.""" rfx_object.close_connection() @@ -388,6 +420,28 @@ def get_device_id( return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string) +def get_device_tuple_from_identifiers( + identifiers: set[tuple[str, str]] +) -> DeviceTuple | None: + """Calculate the device tuple from a device entry.""" + identifier = next((x for x in identifiers if x[0] == DOMAIN), None) + if not identifier: + return None + # work around legacy identifier, being a multi tuple value + identifier2 = cast(tuple[str, str, str, str], identifier) + return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove config entry from a device. + + The actual cleanup is done in the device registry event + """ + return True + + class RfxtrxEntity(RestoreEntity): """Represents a Rfxtrx device. @@ -424,13 +478,6 @@ async def async_added_to_hass(self): ) ) - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{self._device_id}", - functools.partial(self.async_remove, force_remove=True), - ) - ) - @property def should_poll(self): """No polling needed for a RFXtrx switch.""" diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 1ec74c2415a11..549a5c3ccbf40 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -23,7 +23,6 @@ CONF_TYPE, ) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, @@ -35,13 +34,18 @@ async_get_registry as async_get_entity_registry, ) -from . import DOMAIN, DeviceTuple, get_device_id, get_rfx_object +from . import ( + DOMAIN, + DeviceTuple, + get_device_id, + get_device_tuple_from_identifiers, + get_rfx_object, +) from .binary_sensor import supported as binary_supported from .const import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_OFF_DELAY, - CONF_REMOVE_DEVICE, CONF_REPLACE_DEVICE, CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, @@ -61,7 +65,7 @@ class DeviceData(TypedDict): """Dict data representing a device entry.""" - event_code: str + event_code: str | None device_id: DeviceTuple @@ -110,26 +114,6 @@ async def async_step_prompt_options(self, user_input=None): ] self._selected_device_object = get_rfx_object(event_code) return await self.async_step_set_device_options() - if CONF_REMOVE_DEVICE in user_input: - remove_devices = user_input[CONF_REMOVE_DEVICE] - devices = {} - for entry_id in remove_devices: - device_data = self._get_device_data(entry_id) - - event_code = device_data[CONF_EVENT_CODE] - device_id = device_data[CONF_DEVICE_ID] - self.hass.helpers.dispatcher.async_dispatcher_send( - f"{DOMAIN}_{CONF_REMOVE_DEVICE}_{device_id}" - ) - self._device_registry.async_remove_device(entry_id) - if event_code is not None: - devices[event_code] = None - - self.update_config_data( - global_options=self._global_options, devices=devices - ) - - return self.async_create_entry(title="", data={}) if CONF_EVENT_CODE in user_input: self._selected_device_event_code = user_input[CONF_EVENT_CODE] self._selected_device = {} @@ -156,11 +140,6 @@ async def async_step_prompt_options(self, user_input=None): self._device_registry = device_registry self._device_entries = device_entries - remove_devices = { - entry.id: entry.name_by_user if entry.name_by_user else entry.name - for entry in device_entries - } - configure_devices = { entry.id: entry.name_by_user if entry.name_by_user else entry.name for entry in device_entries @@ -174,7 +153,6 @@ async def async_step_prompt_options(self, user_input=None): ): bool, vol.Optional(CONF_EVENT_CODE): str, vol.Optional(CONF_DEVICE): vol.In(configure_devices), - vol.Optional(CONF_REMOVE_DEVICE): cv.multi_select(remove_devices), } return self.async_show_form( @@ -416,15 +394,15 @@ def _get_device_event_code(self, entry_id): def _get_device_data(self, entry_id) -> DeviceData: """Get event code based on device identifier.""" - event_code: str + event_code: str | None = None entry = self._device_registry.async_get(entry_id) assert entry - device_id = cast(DeviceTuple, next(iter(entry.identifiers))[1:]) + device_id = get_device_tuple_from_identifiers(entry.identifiers) + assert device_id for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): if tuple(entity_info.get(CONF_DEVICE_ID)) == device_id: event_code = cast(str, packet_id) break - assert event_code return DeviceData(event_code=event_code, device_id=device_id) @callback diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index b7cb52df98428..50cd355c45799 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -6,7 +6,6 @@ CONF_OFF_DELAY = "off_delay" CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" -CONF_REMOVE_DEVICE = "remove_device" CONF_REPLACE_DEVICE = "replace_device" CONST_VENETIAN_BLIND_MODE_DEFAULT = "Unknown" diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 8ba27cb450ecc..d712551832989 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyRFXtrx==0.27.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["RFXtrx"] } diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index eb3a9ba699c5a..542ff9a45cdcb 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -42,8 +42,7 @@ "debug": "Enable debugging", "automatic_add": "Enable automatic add", "event_code": "Enter event code to add", - "device": "Select device to configure", - "remove_device": "Select device to delete" + "device": "Select device to configure" }, "title": "Rfxtrx Options" }, diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 988beaa8fb601..8bc1fa428744f 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -7,7 +7,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON +from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,8 +17,15 @@ DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry, + get_pt2262_cmd, +) +from .const import ( + COMMAND_OFF_LIST, + COMMAND_ON_LIST, + CONF_DATA_BITS, + CONF_SIGNAL_REPETITIONS, + DEVICE_PACKET_TYPE_LIGHTING4, ) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS DATA_SWITCH = f"{DOMAIN}_switch" @@ -53,6 +60,9 @@ def _constructor( event.device, device_id, entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), + entity_info.get(CONF_DATA_BITS), + entity_info.get(CONF_COMMAND_ON), + entity_info.get(CONF_COMMAND_OFF), event=event if auto else None, ) ] @@ -65,6 +75,22 @@ def _constructor( class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): """Representation of a RFXtrx switch.""" + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + signal_repetitions: int = 1, + data_bits: int | None = None, + cmd_on: int | None = None, + cmd_off: int | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: + """Initialize the RFXtrx switch.""" + super().__init__(device, device_id, signal_repetitions, event=event) + self._data_bits = data_bits + self._cmd_on = cmd_on + self._cmd_off = cmd_off + async def async_added_to_hass(self): """Restore device state.""" await super().async_added_to_hass() @@ -74,15 +100,34 @@ async def async_added_to_hass(self): if old_state is not None: self._state = old_state.state == STATE_ON - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: - """Apply command from rfxtrx.""" + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + """Apply event for a lighting 4 device.""" + if self._data_bits is not None: + cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) + assert cmdstr + cmd = int(cmdstr, 16) + if cmd == self._cmd_on: + self._state = True + elif cmd == self._cmd_off: + self._state = False + else: + self._state = True + + def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, rfxtrxmod.ControlEvent) - super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: self._state = True elif event.values["Command"] in COMMAND_OFF_LIST: self._state = False + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + self._apply_event_lighting4(event) + else: + self._apply_event_standard(event) + @callback def _handle_event( self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple @@ -100,12 +145,18 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the device on.""" - await self._async_send(self._device.send_on) + if self._cmd_on is not None: + await self._async_send(self._device.send_command, self._cmd_on) + else: + await self._async_send(self._device.send_on) self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" - await self._async_send(self._device.send_off) + if self._cmd_off is not None: + await self._async_send(self._device.send_command, self._cmd_off) + else: + await self._async_send(self._device.send_off) self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/translations/el.json b/homeassistant/components/rfxtrx/translations/el.json index 1ad4a767b323f..b04b6172969b0 100644 --- a/homeassistant/components/rfxtrx/translations/el.json +++ b/homeassistant/components/rfxtrx/translations/el.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\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.", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "error": { @@ -9,6 +10,7 @@ "step": { "setup_network": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1" }, "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" @@ -20,6 +22,9 @@ "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "setup_serial_manual_path": { + "data": { + "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, "title": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae" }, "user": { @@ -30,12 +35,24 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "\u0391\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae\u03c2: {subtype}", + "send_status": "\u0391\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2: {subtype}" + }, + "trigger_type": { + "command": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae: {subtype}", + "status": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7: {subtype}" + } + }, "options": { "error": { + "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", "invalid_event_code": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2", "invalid_input_2262_off": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae off", "invalid_input_2262_on": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae on", - "invalid_input_off_delay": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2" + "invalid_input_off_delay": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "prompt_options": { @@ -57,7 +74,8 @@ "off_delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", "off_delay_enabled": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", "replace_device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", - "signal_repetitions": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ae\u03c8\u03b5\u03c9\u03bd \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + "signal_repetitions": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ae\u03c8\u03b5\u03c9\u03bd \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "venetian_blind_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b2\u03b5\u03bd\u03b5\u03c4\u03c3\u03b9\u03ac\u03bd\u03b9\u03ba\u03b7\u03c2 \u03c0\u03b5\u03c1\u03c3\u03af\u03b4\u03b1\u03c2" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } diff --git a/homeassistant/components/rfxtrx/translations/pt-BR.json b/homeassistant/components/rfxtrx/translations/pt-BR.json new file mode 100644 index 0000000000000..6f867a22a5517 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/pt-BR.json @@ -0,0 +1,84 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "setup_network": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "title": "Selecione o endere\u00e7o de conex\u00e3o" + }, + "setup_serial": { + "data": { + "device": "Selecionar dispositivo" + }, + "title": "Dispositivo" + }, + "setup_serial_manual_path": { + "data": { + "device": "Caminho do Dispositivo USB" + }, + "title": "Caminho" + }, + "user": { + "data": { + "type": "Tipo de conex\u00e3o" + }, + "title": "Selecione o tipo de conex\u00e3o" + } + } + }, + "device_automation": { + "action_type": { + "send_command": "Enviar comando: {subtype}", + "send_status": "Enviar atualiza\u00e7\u00e3o de status: {subtype}" + }, + "trigger_type": { + "command": "Comando recebido: {subtype}", + "status": "Status recebido: {subtype}" + } + }, + "options": { + "error": { + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_event_code": "C\u00f3digo de evento inv\u00e1lido", + "invalid_input_2262_off": "Entrada inv\u00e1lida para comando desligado", + "invalid_input_2262_on": "Entrada inv\u00e1lida para comando ligado", + "invalid_input_off_delay": "Entrada inv\u00e1lida para atraso de desligamento", + "unknown": "Erro inesperado" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "Habilitar a adi\u00e7\u00e3o autom\u00e1tica", + "debug": "Habilitar a depura\u00e7\u00e3o", + "device": "Selecione o dispositivo para configurar", + "event_code": "Insira o c\u00f3digo do evento para adicionar", + "remove_device": "Selecione o dispositivo para excluir" + }, + "title": "Op\u00e7\u00f5es de Rfxtrx" + }, + "set_device_options": { + "data": { + "command_off": "Valor de bits de dados para comando desligado", + "command_on": "Valor de bits de dados para comando ligado", + "data_bit": "N\u00famero de bits de dados", + "fire_event": "Ativar evento do dispositivo", + "off_delay": "Atraso de desligamento", + "off_delay_enabled": "Ativar atraso de desligamento", + "replace_device": "Selecione o dispositivo para substituir", + "signal_repetitions": "N\u00famero de repeti\u00e7\u00f5es de sinal", + "venetian_blind_mode": "Modo de persianas" + }, + "title": "Configurar op\u00e7\u00f5es do dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/sk.json b/homeassistant/components/rfxtrx/translations/sk.json new file mode 100644 index 0000000000000..e343d2e8b3188 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "setup_network": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/uk.json b/homeassistant/components/rfxtrx/translations/uk.json index 1b0938b8b7088..65cca65679ce7 100644 --- a/homeassistant/components/rfxtrx/translations/uk.json +++ b/homeassistant/components/rfxtrx/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "already_configured": "\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.", "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "error": { diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index ec763ece1de89..d66b0b1cf7c76 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "already_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index bcb881f37247c..405474f5875cc 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER @@ -81,7 +80,7 @@ async def _async_validate( data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, ) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 4aed69a05f351..e02a0ba65265d 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@bachya" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aioridwell"] } diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json new file mode 100644 index 0000000000000..72df4a968182f --- /dev/null +++ b/homeassistant/components/ridwell/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/el.json b/homeassistant/components/ridwell/translations/el.json new file mode 100644 index 0000000000000..d8f4fa09850d4 --- /dev/null +++ b/homeassistant/components/ridwell/translations/el.json @@ -0,0 +1,28 @@ +{ + "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", + "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_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": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username}:", + "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": { + "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": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/nb.json b/homeassistant/components/ridwell/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/ridwell/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/pt-BR.json b/homeassistant/components/ridwell/translations/pt-BR.json index befa822057f73..c77ca3a02fe80 100644 --- a/homeassistant/components/ridwell/translations/pt-BR.json +++ b/homeassistant/components/ridwell/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado", - "reauth_successful": "A reautentica\u00e7\u00e3o foi feita com sucesso" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", @@ -14,12 +14,12 @@ "password": "Senha" }, "description": "Por favor, digite novamente a senha para {username}:", - "title": "Reautenticar integra\u00e7\u00e3o" + "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "description": "Digite seu nome de usu\u00e1rio e senha:" } diff --git a/homeassistant/components/ridwell/translations/sk.json b/homeassistant/components/ridwell/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/ridwell/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 3e745dc2d4b81..a64411e610f76 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -12,5 +12,6 @@ "macaddress": "0CAE7D*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["ring_doorbell"] } diff --git a/homeassistant/components/ring/translations/el.json b/homeassistant/components/ring/translations/el.json new file mode 100644 index 0000000000000..5d50d06388de6 --- /dev/null +++ b/homeassistant/components/ring/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" + }, + "error": { + "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": { + "2fa": { + "data": { + "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" + }, + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" + }, + "user": { + "data": { + "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" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/pt-BR.json b/homeassistant/components/ring/translations/pt-BR.json index abb894549cc93..e3bbe6cd9d02e 100644 --- a/homeassistant/components/ring/translations/pt-BR.json +++ b/homeassistant/components/ring/translations/pt-BR.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de verifica\u00e7\u00e3o em duas etapas" + }, + "title": "Autentica\u00e7\u00e3o de duas etapas" + }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "title": "Entrar com conta Ring" } } } diff --git a/homeassistant/components/ring/translations/sk.json b/homeassistant/components/ring/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/ring/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index 68adda3edeae1..eee0f3d6a7763 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": ["python-ripple-api==0.0.3"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyripple"] } diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 2e37499b10fce..362fd6157000f 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -80,7 +80,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +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/risco/manifest.json b/homeassistant/components/risco/manifest.json index 2da0a5254a485..736adcf0c3560 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyrisco==0.3.1"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyrisco"] } diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json index c38cfc72cc19d..1879905779305 100644 --- a/homeassistant/components/risco/translations/el.json +++ b/homeassistant/components/risco/translations/el.json @@ -1,6 +1,10 @@ { "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\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, @@ -8,7 +12,8 @@ "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" + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } } @@ -18,6 +23,7 @@ "ha_to_risco": { "data": { "armed_away": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u0395\u03ba\u03c4\u03cc\u03c2", + "armed_custom_bypass": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7", "armed_home": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u0395\u03bd\u03c4\u03cc\u03c2", "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bd\u03cd\u03c7\u03c4\u03b1\u03c2" }, @@ -40,7 +46,9 @@ "D": "\u039f\u03bc\u03ac\u03b4\u03b1 \u0394", "arm": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 (\u0395\u039a\u03a4\u039f\u03a3)", "partial_arm": "\u039c\u03b5\u03c1\u03b9\u03ba\u03ce\u03c2 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 (\u0395\u039d\u03a4\u039f\u03a3)" - } + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03b9 \u03bf \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03b9 \u03b7 Risco", + "title": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03b9\u03c7\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Risco \u03c3\u03b5 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Home Assistant" } } } diff --git a/homeassistant/components/risco/translations/pt-BR.json b/homeassistant/components/risco/translations/pt-BR.json new file mode 100644 index 0000000000000..53659ab672a66 --- /dev/null +++ b/homeassistant/components/risco/translations/pt-BR.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "pin": "C\u00f3digo PIN", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "Armado Fora", + "armed_custom_bypass": "Bypass Armado Personalizado", + "armed_home": "Armado (Casa)", + "armed_night": "Armado (Noite)" + }, + "description": "Selecione o estado para definir seu alarme Risco ao armar o alarme do Home Assistant", + "title": "Mapear os estados do Home Assistant para os estados do Risco" + }, + "init": { + "data": { + "code_arm_required": "C\u00f3digo PIN", + "code_disarm_required": "C\u00f3digo PIN", + "scan_interval": "Quantas vezes pesquisar Risco (em segundos)" + }, + "title": "Configurar op\u00e7\u00f5es" + }, + "risco_to_ha": { + "data": { + "A": "Grupo A", + "B": "Grupo B", + "C": "Grupo C", + "D": "Grupo D", + "arm": "Armado (FORA)", + "partial_arm": "Parcialmente Armado (STAY)" + }, + "description": "Selecione qual estado o alarme do Home Assistant reportar\u00e1 para cada estado reportado pelo Risco", + "title": "Mapear os estados do Risco para os estados do Home Assistant" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sk.json b/homeassistant/components/risco/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/risco/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 2daa6e4387371..6c66b906ff69f 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyrituals==0.0.6"], "codeowners": ["@milanmeu"], "quality_scale": "silver", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyrituals"] } diff --git a/homeassistant/components/rituals_perfume_genie/translations/el.json b/homeassistant/components/rituals_perfume_genie/translations/el.json index 2efdf57dbcb4a..2bc22d498e13d 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/el.json +++ b/homeassistant/components/rituals_perfume_genie/translations/el.json @@ -1,7 +1,19 @@ { "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Rituals" } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/pt-BR.json b/homeassistant/components/rituals_perfume_genie/translations/pt-BR.json new file mode 100644 index 0000000000000..a278ec20ec292 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "title": "Conecte-se \u00e0 sua conta Rituals" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/sk.json b/homeassistant/components/rituals_perfume_genie/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index bcbb96c70349c..db73d5b519bd6 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": ["PyRMVtransport==0.3.3"], "codeowners": ["@cgtobi"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["RMVtransport"] } diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 13e6a7bb745a1..b95eb9e8cca27 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": ["rocketchat-API==0.6.1"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["rocketchat_API"] } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 4d97e8c5fac4c..e6e31f087132e 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,9 +1,13 @@ """Support for Roku.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps import logging +from typing import Any, TypeVar from rokuecp import RokuConnectionError, RokuError +from typing_extensions import Concatenate, ParamSpec from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -12,6 +16,7 @@ from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator +from .entity import RokuEntity CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -19,10 +24,14 @@ Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.REMOTE, + Platform.SELECT, Platform.SENSOR, ] _LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T", bound="RokuEntity") +_P = ParamSpec("_P") + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" @@ -46,10 +55,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def roku_exception_handler(func): +def roku_exception_handler( + func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] """Decorate Roku calls to handle Roku exceptions.""" - async def handler(self, *args, **kwargs): + @wraps(func) + async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) except RokuConnectionError as error: @@ -59,4 +71,4 @@ async def handler(self, *args, **kwargs): if self.available: _LOGGER.error("Invalid response from API: %s", error) - return handler + return wrapper diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 49af47401236e..72b572e8d3e69 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -21,6 +21,7 @@ from homeassistant.helpers.network import is_internal_request from .coordinator import RokuDataUpdateCoordinator +from .helpers import format_channel_name CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_APP: MEDIA_CLASS_APP, @@ -69,12 +70,12 @@ def get_thumbnail_url_full( async def async_browse_media( - hass, + hass: HomeAssistant, coordinator: RokuDataUpdateCoordinator, get_browse_image_url: GetBrowseImageUrlType, media_content_id: str | None, media_content_type: str | None, -): +) -> BrowseMedia: """Browse media.""" if media_content_id is None: return await root_payload( @@ -113,7 +114,7 @@ async def root_payload( hass: HomeAssistant, coordinator: RokuDataUpdateCoordinator, get_browse_image_url: GetBrowseImageUrlType, -): +) -> BrowseMedia: """Return root payload for Roku.""" device = coordinator.data @@ -134,6 +135,9 @@ async def root_payload( ) ) + for child in children: + child.thumbnail = "https://brands.home-assistant.io/_/roku/logo.png" + try: browse_item = await media_source.async_browse_media(hass, None) @@ -191,11 +195,11 @@ def build_item_response( title = "TV Channels" media = [ { - "channel_number": item.number, - "title": item.name, + "channel_number": channel.number, + "title": format_channel_name(channel.number, channel.name), "type": MEDIA_TYPE_CHANNEL, } - for item in coordinator.data.channels + for channel in coordinator.data.channels ] children_media_class = MEDIA_CLASS_CHANNEL @@ -223,7 +227,7 @@ def item_payload( item: dict, coordinator: RokuDataUpdateCoordinator, get_browse_image_url: GetBrowseImageUrlType, -): +) -> BrowseMedia: """ Create response payload for a single media item. diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index dff0f7d53c657..e3b8b97aa8ffc 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from urllib.parse import urlparse from rokuecp import Roku, RokuError @@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistant, data: dict) -> dict: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -44,12 +45,14 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + discovery_info: dict[str, Any] + + def __init__(self) -> None: """Set up the instance.""" self.discovery_info = {} @callback - def _show_form(self, errors: dict | None = None) -> FlowResult: + 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", @@ -57,7 +60,9 @@ def _show_form(self, errors: dict | None = None) -> FlowResult: errors=errors or {}, ) - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index e399f6e0558be..f098483e0c63f 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -2,10 +2,12 @@ DOMAIN = "roku" # Attributes +ATTR_ARTIST_NAME = "artist_name" ATTR_CONTENT_ID = "content_id" ATTR_FORMAT = "format" ATTR_KEYWORD = "keyword" ATTR_MEDIA_TYPE = "media_type" +ATTR_THUMBNAIL = "thumbnail" # Default Values DEFAULT_PORT = 8060 diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index 5b0d76349963d..f084302841eed 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -9,11 +9,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.dt import utcnow from .const import DOMAIN +REQUEST_REFRESH_DELAY = 0.35 + SCAN_INTERVAL = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) @@ -41,6 +44,11 @@ def __init__( _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), ) async def _async_update_data(self) -> Device: diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index 261d0c95445ce..ef969b846aae9 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -17,7 +17,7 @@ class RokuEntity(CoordinatorEntity): def __init__( self, *, - device_id: str, + device_id: str | None, coordinator: RokuDataUpdateCoordinator, description: EntityDescription | None = None, ) -> None: @@ -28,10 +28,11 @@ def __init__( if description is not None: self.entity_description = description self._attr_name = f"{coordinator.data.info.name} {description.name}" - self._attr_unique_id = f"{device_id}_{description.key}" + if device_id is not None: + self._attr_unique_id = f"{device_id}_{description.key}" @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return device information about this Roku device.""" if self._device_id is None: return None diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py new file mode 100644 index 0000000000000..7f507a9fe52c8 --- /dev/null +++ b/homeassistant/components/roku/helpers.py @@ -0,0 +1,10 @@ +"""Helpers for Roku.""" +from __future__ import annotations + + +def format_channel_name(channel_number: str, channel_name: str | None = None) -> str: + """Format a Roku Channel name.""" + if channel_name is not None and channel_name != "": + return f"{channel_name} ({channel_number})" + + return channel_number diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 7dd5974589c3e..4918e7742be17 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.12.0"], + "requirements": ["rokuecp==0.14.1"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, @@ -16,5 +16,6 @@ "codeowners": ["@ctalkington"], "quality_scale": "silver", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["rokuecp"] } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 67abae262d530..9cf17d890a4d5 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -3,23 +3,27 @@ import datetime as dt import logging +import mimetypes from typing import Any -from urllib.parse import quote +from rokuecp.helpers import guess_stream_format import voluptuous as vol +import yarl from homeassistant.components import media_source -from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, + async_process_play_media_url, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_EXTRA, MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, + MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -46,20 +50,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.network import get_url from . import roku_exception_handler from .browse_media import async_browse_media from .const import ( + ATTR_ARTIST_NAME, ATTR_CONTENT_ID, ATTR_FORMAT, ATTR_KEYWORD, ATTR_MEDIA_TYPE, + ATTR_THUMBNAIL, DOMAIN, SERVICE_SEARCH, ) from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity +from .helpers import format_channel_name _LOGGER = logging.getLogger(__name__) @@ -77,21 +83,36 @@ | SUPPORT_BROWSE_MEDIA ) + +STREAM_FORMAT_TO_MEDIA_TYPE = { + "dash": MEDIA_TYPE_VIDEO, + "hls": MEDIA_TYPE_VIDEO, + "ism": MEDIA_TYPE_VIDEO, + "m4a": MEDIA_TYPE_MUSIC, + "m4v": MEDIA_TYPE_VIDEO, + "mka": MEDIA_TYPE_MUSIC, + "mkv": MEDIA_TYPE_VIDEO, + "mks": MEDIA_TYPE_VIDEO, + "mp3": MEDIA_TYPE_MUSIC, + "mp4": MEDIA_TYPE_VIDEO, +} + ATTRS_TO_LAUNCH_PARAMS = { ATTR_CONTENT_ID: "contentID", - ATTR_MEDIA_TYPE: "MediaType", + ATTR_MEDIA_TYPE: "mediaType", } -PLAY_MEDIA_SUPPORTED_TYPES = ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_URL, - FORMAT_CONTENT_TYPE[HLS_PROVIDER], -) - -ATTRS_TO_PLAY_VIDEO_PARAMS = { +ATTRS_TO_PLAY_ON_ROKU_PARAMS = { ATTR_NAME: "videoName", ATTR_FORMAT: "videoFormat", + ATTR_THUMBNAIL: "k", +} + +ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS = { + ATTR_NAME: "songName", + ATTR_FORMAT: "songFormat", + ATTR_ARTIST_NAME: "artistName", + ATTR_THUMBNAIL: "albumArtUrl", } SEARCH_SCHEMA = {vol.Required(ATTR_KEYWORD): str} @@ -117,7 +138,9 @@ async def async_setup_entry( class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" - def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: + def __init__( + self, unique_id: str | None, coordinator: RokuDataUpdateCoordinator + ) -> None: """Initialize the Roku device.""" super().__init__( coordinator=coordinator, @@ -212,10 +235,9 @@ def media_channel(self) -> str | None: if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None: return None - if self.coordinator.data.channel.name is not None: - return f"{self.coordinator.data.channel.name} ({self.coordinator.data.channel.number})" + channel = self.coordinator.data.channel - return self.coordinator.data.channel.number + return format_channel_name(channel.number, channel.name) @property def media_title(self) -> str | None: @@ -231,7 +253,7 @@ def media_title(self) -> str | None: @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - if self._media_playback_trackable(): + if self.coordinator.data.media is not None and self._media_playback_trackable(): return self.coordinator.data.media.duration return None @@ -239,7 +261,7 @@ def media_duration(self) -> int | None: @property def media_position(self) -> int | None: """Position of current playing media in seconds.""" - if self._media_playback_trackable(): + if self.coordinator.data.media is not None and self._media_playback_trackable(): return self.coordinator.data.media.position return None @@ -247,7 +269,7 @@ def media_position(self) -> int | None: @property def media_position_updated_at(self) -> dt.datetime | None: """When was the position of the current playing media valid.""" - if self._media_playback_trackable(): + if self.coordinator.data.media is not None and self._media_playback_trackable(): return self.coordinator.data.media.at return None @@ -263,10 +285,12 @@ def source(self) -> str | None: @property def source_list(self) -> list: """List of available input sources.""" - return ["Home"] + sorted(app.name for app in self.coordinator.data.apps) + return ["Home"] + sorted( + app.name for app in self.coordinator.data.apps if app.name is not None + ) @roku_exception_handler - async def search(self, keyword): + async def search(self, keyword: str) -> None: """Emulate opening the search screen and entering the search keyword.""" await self.coordinator.roku.search(keyword) @@ -343,7 +367,7 @@ async def async_media_next_track(self) -> None: await self.coordinator.async_request_refresh() @roku_exception_handler - async def async_mute_volume(self, mute) -> None: + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.coordinator.roku.remote("volume_mute") await self.coordinator.async_request_refresh() @@ -359,37 +383,72 @@ async def async_volume_down(self) -> None: await self.coordinator.roku.remote("volume_down") @roku_exception_handler - async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media from a URL or file, launch an application, or tune to a channel.""" extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} + original_media_type: str = media_type + original_media_id: str = media_id + mime_type: str | None = None + stream_name: str | None = None + stream_format: str | None = extra.get(ATTR_FORMAT) # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media(self.hass, media_id) media_type = MEDIA_TYPE_URL media_id = sourced_media.url - - # Sign and prefix with URL if playing a relative URL - if media_id[0] == "/": - media_id = async_sign_path( - self.hass, - quote(media_id), - dt.timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), - ) - - # prepend external URL - hass_url = get_url(self.hass) - media_id = f"{hass_url}{media_id}" - - if media_type not in PLAY_MEDIA_SUPPORTED_TYPES: - _LOGGER.error( - "Invalid media type %s. Only %s, %s, %s, and camera HLS streams are supported", - media_type, - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_URL, - ) - return + mime_type = sourced_media.mime_type + stream_name = original_media_id + stream_format = guess_stream_format(media_id, mime_type) + + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) + + if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: + media_type = MEDIA_TYPE_VIDEO + mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + stream_name = "Camera Stream" + stream_format = "hls" + + if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + parsed = yarl.URL(media_id) + + if mime_type is None: + mime_type, _ = mimetypes.guess_type(parsed.path) + + if stream_format is None: + stream_format = guess_stream_format(media_id, mime_type) + + if extra.get(ATTR_FORMAT) is None: + extra[ATTR_FORMAT] = stream_format + + if extra[ATTR_FORMAT] not in STREAM_FORMAT_TO_MEDIA_TYPE: + _LOGGER.error( + "Media type %s is not supported with format %s (mime: %s)", + original_media_type, + extra[ATTR_FORMAT], + mime_type, + ) + return + + if ( + media_type == MEDIA_TYPE_URL + and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MEDIA_TYPE_MUSIC + ): + media_type = MEDIA_TYPE_MUSIC + + if media_type == MEDIA_TYPE_MUSIC and "tts_proxy" in media_id: + stream_name = "Text to Speech" + elif stream_name is None: + if stream_format == "ism": + stream_name = parsed.parts[-2] + else: + stream_name = parsed.name + + if extra.get(ATTR_NAME) is None: + extra[ATTR_NAME] = stream_name if media_type == MEDIA_TYPE_APP: params = { @@ -401,20 +460,30 @@ async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> No await self.coordinator.roku.launch(media_id, params) elif media_type == MEDIA_TYPE_CHANNEL: await self.coordinator.roku.tune(media_id) - elif media_type == MEDIA_TYPE_URL: + elif media_type == MEDIA_TYPE_MUSIC: + if extra.get(ATTR_ARTIST_NAME) is None: + extra[ATTR_ARTIST_NAME] = "Home Assistant" + params = { param: extra[attr] - for (attr, param) in ATTRS_TO_PLAY_VIDEO_PARAMS.items() + for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS.items() if attr in extra } + params = {"t": "a", **params} + await self.coordinator.roku.play_on_roku(media_id, params) - elif media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: + elif media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): params = { - "MediaType": "hls", + param: extra[attr] + for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() + if attr in extra } await self.coordinator.roku.play_on_roku(media_id, params) + else: + _LOGGER.error("Media type %s is not supported", original_media_type) + return await self.coordinator.async_request_refresh() @@ -433,7 +502,6 @@ async def async_select_source(self, source: str) -> None: None, ) - if appl is not None: + if appl is not None and appl.app_id is not None: await self.coordinator.roku.launch(appl.app_id) - - await self.coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 8f0d39ed1d90e..9a0cd6f51e3df 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,6 +1,9 @@ """Support for the Roku remote.""" from __future__ import annotations +from collections.abc import Iterable +from typing import Any + from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -42,19 +45,19 @@ def is_on(self) -> bool: return not self.coordinator.data.state.standby @roku_exception_handler - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.coordinator.roku.remote("poweron") await self.coordinator.async_request_refresh() @roku_exception_handler - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.coordinator.roku.remote("poweroff") await self.coordinator.async_request_refresh() @roku_exception_handler - async def async_send_command(self, command: list, **kwargs) -> None: + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py new file mode 100644 index 0000000000000..9120a4fe9cef2 --- /dev/null +++ b/homeassistant/components/roku/select.py @@ -0,0 +1,174 @@ +"""Support for Roku selects.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from rokuecp import Roku +from rokuecp.models import Device as RokuDevice + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import roku_exception_handler +from .const import DOMAIN +from .coordinator import RokuDataUpdateCoordinator +from .entity import RokuEntity +from .helpers import format_channel_name + + +@dataclass +class RokuSelectEntityDescriptionMixin: + """Mixin for required keys.""" + + options_fn: Callable[[RokuDevice], list[str]] + value_fn: Callable[[RokuDevice], str | None] + set_fn: Callable[[RokuDevice, Roku, str], Awaitable[None]] + + +def _get_application_name(device: RokuDevice) -> str | None: + if device.app is None or device.app.name is None: + return None + + if device.app.name == "Roku": + return "Home" + + return device.app.name + + +def _get_applications(device: RokuDevice) -> list[str]: + return ["Home"] + sorted(app.name for app in device.apps if app.name is not None) + + +def _get_channel_name(device: RokuDevice) -> str | None: + if device.channel is None: + return None + + return format_channel_name(device.channel.number, device.channel.name) + + +def _get_channels(device: RokuDevice) -> list[str]: + return sorted( + format_channel_name(channel.number, channel.name) for channel in device.channels + ) + + +async def _launch_application(device: RokuDevice, roku: Roku, value: str) -> None: + if value == "Home": + await roku.remote("home") + + appl = next( + (app for app in device.apps if value == app.name), + None, + ) + + if appl is not None and appl.app_id is not None: + await roku.launch(appl.app_id) + + +async def _tune_channel(device: RokuDevice, roku: Roku, value: str) -> None: + _channel = next( + ( + channel + for channel in device.channels + if ( + channel.name is not None + and value == format_channel_name(channel.number, channel.name) + ) + or value == channel.number + ), + None, + ) + + if _channel is not None: + await roku.tune(_channel.number) + + +@dataclass +class RokuSelectEntityDescription( + SelectEntityDescription, RokuSelectEntityDescriptionMixin +): + """Describes Roku select entity.""" + + +ENTITIES: tuple[RokuSelectEntityDescription, ...] = ( + RokuSelectEntityDescription( + key="application", + name="Application", + icon="mdi:application", + set_fn=_launch_application, + value_fn=_get_application_name, + options_fn=_get_applications, + entity_registry_enabled_default=False, + ), +) + +CHANNEL_ENTITY = RokuSelectEntityDescription( + key="channel", + name="Channel", + icon="mdi:television", + set_fn=_tune_channel, + value_fn=_get_channel_name, + options_fn=_get_channels, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Roku select based on a config entry.""" + coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + device: RokuDevice = coordinator.data + unique_id = device.info.serial_number + + entities: list[RokuSelectEntity] = [] + + for description in ENTITIES: + entities.append( + RokuSelectEntity( + device_id=unique_id, + coordinator=coordinator, + description=description, + ) + ) + + if len(device.channels) > 0: + entities.append( + RokuSelectEntity( + device_id=unique_id, + coordinator=coordinator, + description=CHANNEL_ENTITY, + ) + ) + + async_add_entities(entities) + + +class RokuSelectEntity(RokuEntity, SelectEntity): + """Defines a Roku select entity.""" + + entity_description: RokuSelectEntityDescription + + @property + def current_option(self) -> str | None: + """Return the current value.""" + return self.entity_description.value_fn(self.coordinator.data) + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return self.entity_description.options_fn(self.coordinator.data) + + @roku_exception_handler + async def async_select_option(self, option: str) -> None: + """Set the option.""" + await self.entity_description.set_fn( + self.coordinator.data, + self.coordinator.roku, + option, + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/roku/translations/el.json b/homeassistant/components/roku/translations/el.json new file mode 100644 index 0000000000000..91087c73a58a8 --- /dev/null +++ b/homeassistant/components/roku/translations/el.json @@ -0,0 +1,29 @@ +{ + "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", + "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" + }, + "flow_title": "{name}", + "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};", + "title": "Roku" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 Roku \u03c3\u03b1\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/pt-BR.json b/homeassistant/components/roku/translations/pt-BR.json index d866e4d4ee2d3..408bbc915c049 100644 --- a/homeassistant/components/roku/translations/pt-BR.json +++ b/homeassistant/components/roku/translations/pt-BR.json @@ -1,12 +1,27 @@ { "config": { - "flow_title": "Roku: {name}", + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", "step": { + "discovery_confirm": { + "description": "Deseja configurar {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Voc\u00ea quer configurar o {name}?", "title": "Roku" }, "user": { + "data": { + "host": "Nome do host" + }, "description": "Digite suas informa\u00e7\u00f5es de Roku." } } diff --git a/homeassistant/components/roku/translations/sk.json b/homeassistant/components/roku/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/roku/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index dc16e92b23792..0a58effa481f0 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -111,7 +111,7 @@ async def async_disconnect_or_timeout(hass, roomba): return True -async def async_update_options(hass, config_entry): +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 6313c800dea58..70053e8ee40e9 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -19,5 +19,6 @@ "macaddress": "DCF505*" } ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["paho_mqtt", "roombapy"] } diff --git a/homeassistant/components/roomba/translations/el.json b/homeassistant/components/roomba/translations/el.json index dad23eecdcf35..e59ce426deb86 100644 --- a/homeassistant/components/roomba/translations/el.json +++ b/homeassistant/components/roomba/translations/el.json @@ -1,16 +1,49 @@ { "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", "not_irobot_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 iRobot", "short_blid": "\u03a4\u03bf BLID \u03c0\u03b5\u03c1\u03b9\u03ba\u03cc\u03c0\u03b7\u03ba\u03b5" }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "flow_title": "{name} ({host})", "step": { + "init": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Roomba \u03ae Braava.", + "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "link": { + "description": "\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 \u03c0\u03bb\u03ae\u03ba\u03c4\u03c1\u03bf Home \u03c3\u03c4\u03bf {name} \u03bc\u03ad\u03c7\u03c1\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03b9 \u03ad\u03bd\u03b1\u03bd \u03ae\u03c7\u03bf (\u03c0\u03b5\u03c1\u03af\u03c0\u03bf\u03c5 \u03b4\u03cd\u03bf \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1) \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c5\u03c0\u03bf\u03b2\u03ac\u03bb\u03b5\u03c4\u03b5 \u03b5\u03bd\u03c4\u03cc\u03c2 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03c9\u03bd.", + "title": "\u0391\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd" + }, + "link_manual": { + "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 \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {auth_help_url}", + "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03b5\u03af Roomba \u03ae Braava \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", + "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, "user": { "data": { "blid": "BLID", "continuous": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03ae\u03c2", - "delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7" + "delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "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" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Roomba \u03ae Braava.", "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/roomba/translations/pt-BR.json b/homeassistant/components/roomba/translations/pt-BR.json index a148d1976adfa..ee0a29eac896d 100644 --- a/homeassistant/components/roomba/translations/pt-BR.json +++ b/homeassistant/components/roomba/translations/pt-BR.json @@ -1,16 +1,51 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot", + "short_blid": "O BLID foi truncado" + }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente" + "cannot_connect": "Falha ao conectar" }, + "flow_title": "{name} ( {host} )", "step": { + "init": { + "data": { + "host": "Nome do host" + }, + "description": "Selecione um Roomba ou Braava.", + "title": "Conecte-se automaticamente ao dispositivo" + }, + "link": { + "description": "Pressione e segure o bot\u00e3o Home em {name} at\u00e9 que o dispositivo gere um som (cerca de dois segundos) e envie em 30 segundos.", + "title": "Recuperar Senha" + }, + "link_manual": { + "data": { + "password": "Senha" + }, + "description": "A senha do dispositivo n\u00e3o p\u00f4de ser recuperada automaticamente. Siga as etapas descritas na documenta\u00e7\u00e3o em: {auth_help_url}", + "title": "Digite a senha" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Nome do host" + }, + "description": "Nenhum Roomba ou Braava foi descoberto em sua rede.", + "title": "Conecte-se manualmente ao dispositivo" + }, "user": { "data": { "blid": "BLID", "continuous": "Cont\u00ednuo", - "delay": "Atraso" + "delay": "Atraso", + "host": "Nome do host", + "password": "Senha" }, - "description": "Atualmente, a recupera\u00e7\u00e3o do BLID e da senha \u00e9 um processo manual. Siga as etapas descritas na documenta\u00e7\u00e3o em: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "Selecione um Roomba ou Braava.", "title": "Conecte-se ao dispositivo" } } diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index f48645717357e..a3b22a3c2cc5e 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": ["roonapi==0.0.38"], "codeowners": ["@pavoni"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["roonapi"] } diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 873f82d4f68b4..16fca72db26b5 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -1,11 +1,21 @@ { "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": { + "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": { "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon" }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \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 \u03ae \u03c4\u03b7\u03bd IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon." } } diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index 39538d2bf5232..a96222b15f3eb 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 foi configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Ocorreu um erro inexperado" + "unknown": "Erro inesperado" }, "step": { "link": { @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Por favor, digite seu hostname ou IP do servidor Roon." } diff --git a/homeassistant/components/roon/translations/sk.json b/homeassistant/components/roon/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/roon/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 3320f9021681a..83a8c0250148d 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": ["boto3==1.20.24"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["boto3", "botocore", "s3transfer"] } diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 27421b2093695..01f2e2703e88b 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": ["rova==0.2.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["rova"] } diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index d09c21779fe80..f8db41b1a31ca 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": ["RPi.GPIO==0.7.1a4"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["RPi"] } diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index ea0bdbcb0f357..403f607e37937 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -2,7 +2,8 @@ "domain": "rpi_gpio_pwm", "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", - "requirements": ["pwmled==1.6.7"], + "requirements": ["pwmled==1.6.10"], "codeowners": ["@soldag"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["adafruit_blinka", "adafruit_circuitpython_pca9685", "pwmled"] } diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index 9e8f0a30e87f7..7f72a7ba77d10 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pifacedigitalio"] } diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 82457d5b29607..ed8a45822b0a8 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Raspberry Pi Power Supply Checker.""" from __future__ import annotations +from collections.abc import Awaitable from typing import Any from rpi_bad_power import new_under_voltage @@ -18,7 +19,7 @@ async def _async_supported(hass: HomeAssistant) -> bool: return under_voltage is not None -class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): +class RPiPowerFlow(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): """Discovery flow handler.""" VERSION = 1 @@ -35,7 +36,7 @@ async def async_step_onboarding( self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by onboarding.""" - has_devices = await self._discovery_function(self.hass) # type: ignore + has_devices = await self._discovery_function(self.hass) if not has_devices: return self.async_abort(reason="no_devices_found") diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json index 34e249ccfc33b..ef20651843e65 100644 --- a/homeassistant/components/rpi_power/manifest.json +++ b/homeassistant/components/rpi_power/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@shenxn", "@swetoast"], "requirements": ["rpi-bad-power==0.1.0"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["rpi_bad_power"] } diff --git a/homeassistant/components/rpi_power/translations/cs.json b/homeassistant/components/rpi_power/translations/cs.json index 86d49fd1f60a6..cc203d3a8a5e7 100644 --- a/homeassistant/components/rpi_power/translations/cs.json +++ b/homeassistant/components/rpi_power/translations/cs.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "Nelze naj\u00edt t\u0159\u00eddu syst\u00e9mu pot\u0159ebnou pro tuto komponentu, ujist\u011bte se, \u017ee je va\u0161e j\u00e1dro aktu\u00e1ln\u00ed a hardware podporov\u00e1n", - "single_instance_allowed": "Ji\u017e je nastaveno. Je mo\u017en\u00e1 pouze jedna konfigurace." + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "step": { "confirm": { - "description": "Chcete zah\u00e1jit nastaven\u00ed?" + "description": "Chcete za\u010d\u00edt nastavovat?" } } }, diff --git a/homeassistant/components/rpi_power/translations/pt-BR.json b/homeassistant/components/rpi_power/translations/pt-BR.json new file mode 100644 index 0000000000000..f886cab722aea --- /dev/null +++ b/homeassistant/components/rpi_power/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "N\u00e3o \u00e9 poss\u00edvel encontrar a classe de sistema necess\u00e1ria para este componente, verifique se o kernel \u00e9 recente e se o hardware \u00e9 suportado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Verificador de fonte de alimenta\u00e7\u00e3o Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/sk.json b/homeassistant/components/rpi_power/translations/sk.json new file mode 100644 index 0000000000000..d19ecb226982c --- /dev/null +++ b/homeassistant/components/rpi_power/translations/sk.json @@ -0,0 +1,13 @@ +{ + "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" + }, + "step": { + "confirm": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + }, + "title": "Kontrola nap\u00e1jacieho zdroja Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/uk.json b/homeassistant/components/rpi_power/translations/uk.json index b60160e1c4ec3..39b0dee9bdb33 100644 --- a/homeassistant/components/rpi_power/translations/uk.json +++ b/homeassistant/components/rpi_power/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u043a\u043b\u0430\u0441, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u0440\u043e\u0431\u043e\u0442\u0438 \u0446\u044c\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0430\u0439\u043d\u043e\u0432\u0456\u0448\u0435 \u044f\u0434\u0440\u043e \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0435 \u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/rpi_power/translations/zh-Hant.json b/homeassistant/components/rpi_power/translations/zh-Hant.json index 05cdeb6852b85..dd2658a56db4d 100644 --- a/homeassistant/components/rpi_power/translations/zh-Hant.json +++ b/homeassistant/components/rpi_power/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u627e\u4e0d\u5230\u7cfb\u7d71\u6240\u9700\u7684\u5143\u4ef6\uff0c\u8acb\u78ba\u5b9a Kernel \u70ba\u6700\u65b0\u7248\u672c\u3001\u540c\u6642\u786c\u9ad4\u70ba\u652f\u63f4\u72c0\u614b", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 3b88f9c02883c..4dcbf7fe048e7 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -81,24 +81,31 @@ async def get(self, request, entity_id=None): """Generate the RSS view XML.""" response = '\n\n' - response += "\n" + response += '\n' + response += " \n" if self._title is not None: - response += " %s\n" % escape( + response += " %s\n" % escape( self._title.async_render(parse_result=False) ) + else: + response += " Home Assistant\n" + + response += " https://www.home-assistant.io/integrations/rss_feed_template/\n" + response += " Home automation feed\n" for item in self._items: - response += " \n" + response += " \n" if "title" in item: - response += " " + response += " <title>" response += escape(item["title"].async_render(parse_result=False)) response += "\n" if "description" in item: - response += " " + response += " " response += escape(item["description"].async_render(parse_result=False)) response += "\n" - response += " \n" + response += " \n" + response += " \n" response += "\n" return web.Response(body=response, content_type=CONTENT_TYPE_XML) diff --git a/homeassistant/components/rtsp_to_webrtc/manifest.json b/homeassistant/components/rtsp_to_webrtc/manifest.json index cf147df4fe641..d3a56ebdee6bb 100644 --- a/homeassistant/components/rtsp_to_webrtc/manifest.json +++ b/homeassistant/components/rtsp_to_webrtc/manifest.json @@ -8,5 +8,6 @@ "codeowners": [ "@allenporter" ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["rtsp_to_webrtc"] } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/el.json b/homeassistant/components/rtsp_to_webrtc/translations/el.json index bc8212e318bb7..0e4c6baa28761 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/el.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/el.json @@ -2,7 +2,8 @@ "config": { "abort": { "server_failure": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 RTSPtoWebRTC \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \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.", - "server_unreachable": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae RTSPtoWebRTC. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \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." + "server_unreachable": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae RTSPtoWebRTC. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \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.", + "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": { "invalid_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae RTSPtoWebRTC, \u03c0.\u03c7. https://example.com.", diff --git a/homeassistant/components/rtsp_to_webrtc/translations/fr.json b/homeassistant/components/rtsp_to_webrtc/translations/fr.json index be13928611364..1235f36d26aa2 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/fr.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/fr.json @@ -12,7 +12,8 @@ }, "step": { "hassio_confirm": { - "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte au serveur RTSPtoWebRTC fourni par l'add-on\u00a0: {addon}\u00a0?" + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte au serveur RTSPtoWebRTC fourni par l'add-on\u00a0: {addon}\u00a0?", + "title": "RTSPtoWebRTC via le module compl\u00e9mentaire Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/id.json b/homeassistant/components/rtsp_to_webrtc/translations/id.json index 3a870e47986bf..105e4072300a7 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/id.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/id.json @@ -1,7 +1,27 @@ { "config": { "abort": { + "server_failure": "Server RTSPtoWebRTC mengembalikan kesalahan. Periksa log untuk informasi lebih lanjut.", + "server_unreachable": "Tidak dapat berkomunikasi dengan server RTSPtoWebRTC. Periksa log untuk informasi lebih lanjut.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_url": "Harus menjadi URL server RTSPtoWebRTC yang valid misalnya https://example.com", + "server_failure": "Server RTSPtoWebRTC mengembalikan kesalahan. Periksa log untuk informasi lebih lanjut.", + "server_unreachable": "Tidak dapat berkomunikasi dengan server RTSPtoWebRTC. Periksa log untuk informasi lebih lanjut." + }, + "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke server RTSPtoWebRTC yang disediakan oleh add-on: {addon}?", + "title": "RTSPtoWebRTC melalui add-on Home Assistant" + }, + "user": { + "data": { + "server_url": "URL server RTSPtoWebRTC misalnya https://example.com" + }, + "description": "Integrasi RTSPtoWebRTC membutuhkan server untuk menerjemahkan aliran RTSP ke WebRTC. Masukkan URL ke server RTSPtoWebRTC.", + "title": "Konfigurasikan RTSPtoWebrTC" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/it.json b/homeassistant/components/rtsp_to_webrtc/translations/it.json index 1247b8be42b1e..c91e0bc34e827 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/it.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/it.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "Si desidera configurare Home Assistant per la connessione al server RTSPtoWebRTC fornito dal componente aggiuntivo: {addon}?", + "description": "Vuoi configurare Home Assistant per la connessione al server RTSPtoWebRTC fornito dal componente aggiuntivo: {addon}?", "title": "RTSPtoWebRTC tramite il componente aggiuntivo di Home Assistant" }, "user": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index 256ae5aa6a85e..6359d66f50a3b 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "server_failure": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u304c\u30a8\u30e9\u30fc\u3092\u8fd4\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", + "server_unreachable": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u3068\u306e\u901a\u4fe1\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/nl.json b/homeassistant/components/rtsp_to_webrtc/translations/nl.json index 57d4fd851d2d9..6b3344f2b6b0b 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/nl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/nl.json @@ -12,6 +12,7 @@ }, "step": { "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met de RTSPtoWebRTC-server die wordt geleverd door de add-on: {addon}?", "title": "RTSPtoWebRTC via Home Assistant add-on" }, "user": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pl.json b/homeassistant/components/rtsp_to_webrtc/translations/pl.json index 34307edfce06f..25f3ffe785f04 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/pl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "server_failure": "Serwer RTSPtoWebRTC zwr\u00f3ci\u0142 b\u0142\u0105d. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji.", + "server_unreachable": "Nie mo\u017cna nawi\u0105za\u0107 komunikacji z serwerem RTSPtoWebRTC. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json b/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json new file mode 100644 index 0000000000000..7856079886272 --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "server_failure": "O servidor RTSPtoWebRTC retornou um erro. Verifique os logs para obter mais informa\u00e7\u00f5es.", + "server_unreachable": "N\u00e3o \u00e9 poss\u00edvel se comunicar com o servidor RTSPtoWebRTC. Verifique os logs para obter mais informa\u00e7\u00f5es.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_url": "Deve ser um URL de servidor RTSPtoWebRTC v\u00e1lido, por exemplo, https://example.com", + "server_failure": "O servidor RTSPtoWebRTC retornou um erro. Verifique os logs para obter mais informa\u00e7\u00f5es.", + "server_unreachable": "N\u00e3o \u00e9 poss\u00edvel se comunicar com o servidor RTSPtoWebRTC. Verifique os logs para obter mais informa\u00e7\u00f5es." + }, + "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao servidor RTSPtoWebRTC fornecido pelo complemento: {addon} ?", + "title": "RTSPtoWebRTC via complemento do Home Assistant" + }, + "user": { + "data": { + "server_url": "URL do servidor RTSPtoWebRTC, por exemplo, https://example.com" + }, + "description": "A integra\u00e7\u00e3o RTSPtoWebRTC requer um servidor para traduzir fluxos RTSP em WebRTC. Insira a URL para o servidor RTSPtoWebRTC.", + "title": "Configurar RTSPtoWebRTC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json b/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json index 60da2aebd3b4d..ace2e23312b11 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "RTSPtoWebRTC \u4f3a\u670d\u5668\u56de\u5831\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002", "server_unreachable": "\u7121\u6cd5\u8207 RTSPtoWebRTC \u4f3a\u670d\u5668\u9032\u884c\u9023\u7dda\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_url": "\u5fc5\u9808\u70ba\u6709\u6548 RTSPtoWebRTC \u4f3a\u670d\u5668 URL\uff0c\u4f8b\u5982\uff1ahttps://example.com", diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index b8b2ef6e46a80..f010d3401471f 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", "requirements": ["pyruckus==0.12"], "codeowners": ["@gabe565"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pexpect", "pyruckus"] } diff --git a/homeassistant/components/ruckus_unleashed/translations/el.json b/homeassistant/components/ruckus_unleashed/translations/el.json new file mode 100644 index 0000000000000..877622243c8a8 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/el.json @@ -0,0 +1,21 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/pt-BR.json b/homeassistant/components/ruckus_unleashed/translations/pt-BR.json new file mode 100644 index 0000000000000..93beddb92a851 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/sk.json b/homeassistant/components/ruckus_unleashed/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index a12d149550b13..4b9b7a2c8d030 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": ["russound_rio==0.1.7"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["russound_rio"] } diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 0e7928fb23b05..f8aea92b0a0fc 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": ["russound==0.1.9"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["russound"] } diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 3cebd37bf5d4d..e8da8738b5b41 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -297,6 +297,7 @@ def success(): async_setup_sabnzbd(hass, sab_api, config, config.get(CONF_NAME, DEFAULT_NAME)) _CONFIGURING[host] = configurator.async_request_config( + hass, DEFAULT_NAME, async_configuration_callback, description="Enter the API Key", diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 25dfe6788009c..08fb1388b38e8 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["configurator"], "after_dependencies": ["discovery"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysabnzbd"] } diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 79067e47c731c..eaa0121f1dd31 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": ["pysaj==0.0.16"], "codeowners": ["@fredericvl"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysaj"] } diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 515e5c0de96b0..508ed2f876ffc 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -24,7 +24,6 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util.async_ import run_callback_threadsafe from .bridge import ( SamsungTVBridge, @@ -101,10 +100,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _async_get_device_bridge( - data: dict[str, Any] + hass: HomeAssistant, data: dict[str, Any] ) -> SamsungTVLegacyBridge | SamsungTVWSBridge: """Get device bridge.""" return SamsungTVBridge.get_bridge( + hass, data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], @@ -119,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bridge = await _async_create_bridge_with_updated_data(hass, entry) # Ensure new token gets saved against the config_entry + @callback def _update_token() -> None: """Update config entry with the new token.""" hass.config_entries.async_update_entry( @@ -127,13 +128,13 @@ def _update_token() -> None: def new_token_callback() -> None: """Update config entry with the new token.""" - run_callback_threadsafe(hass.loop, _update_token) + hass.add_job(_update_token) bridge.register_new_token_callback(new_token_callback) - def stop_bridge(event: Event) -> None: + async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" - bridge.stop() + await bridge.async_stop() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) @@ -169,14 +170,14 @@ async def _async_create_bridge_with_updated_data( updated_data[CONF_PORT] = port updated_data[CONF_METHOD] = method - bridge = _async_get_device_bridge({**entry.data, **updated_data}) + bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) mac = entry.data.get(CONF_MAC) if not mac and bridge.method == METHOD_WEBSOCKET: if info: mac = mac_from_device_info(info) else: - mac = await hass.async_add_executor_job(bridge.mac_from_device) + mac = await bridge.async_mac_from_device() if not mac: mac = await hass.async_add_executor_job( @@ -196,7 +197,7 @@ 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: - hass.data[DOMAIN][entry.entry_id].stop() + await hass.data[DOMAIN][entry.entry_id].async_stop() return unload_ok diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 64705435fc59d..74daf1d34e04e 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -54,25 +54,18 @@ async def async_get_device_info( hass: HomeAssistant, bridge: SamsungTVWSBridge | SamsungTVLegacyBridge | None, host: str, -) -> tuple[int | None, str | None, dict[str, Any] | None]: - """Fetch the port, method, and device info.""" - return await hass.async_add_executor_job(_get_device_info, bridge, host) - - -def _get_device_info( - bridge: SamsungTVWSBridge | SamsungTVLegacyBridge, host: str ) -> tuple[int | None, str | None, dict[str, Any] | None]: """Fetch the port, method, and device info.""" if bridge and bridge.port: - return bridge.port, bridge.method, bridge.device_info() + return bridge.port, bridge.method, await bridge.async_device_info() for port in WEBSOCKET_PORTS: - bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port) - if info := bridge.device_info(): + bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port) + if info := await bridge.async_device_info(): return port, METHOD_WEBSOCKET, info - bridge = SamsungTVBridge.get_bridge(METHOD_LEGACY, host, LEGACY_PORT) - result = bridge.try_connect() + bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT) + result = await bridge.async_try_connect() if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): return LEGACY_PORT, METHOD_LEGACY, None @@ -84,15 +77,22 @@ class SamsungTVBridge(ABC): @staticmethod def get_bridge( - method: str, host: str, port: int | None = None, token: str | None = None + hass: HomeAssistant, + method: str, + host: str, + port: int | None = None, + token: str | None = None, ) -> SamsungTVLegacyBridge | SamsungTVWSBridge: """Get Bridge instance.""" if method == METHOD_LEGACY or port == LEGACY_PORT: - return SamsungTVLegacyBridge(method, host, port) - return SamsungTVWSBridge(method, host, port, token) + return SamsungTVLegacyBridge(hass, method, host, port) + return SamsungTVWSBridge(hass, method, host, port, token) - def __init__(self, method: str, host: str, port: int | None = None) -> None: + def __init__( + self, hass: HomeAssistant, method: str, host: str, port: int | None = None + ) -> None: """Initialize Bridge.""" + self.hass = hass self.port = port self.method = method self.host = host @@ -110,24 +110,29 @@ def register_new_token_callback(self, func: CALLBACK_TYPE) -> None: self._new_token_callback = func @abstractmethod - def try_connect(self) -> str | None: + async def async_try_connect(self) -> str | None: """Try to connect to the TV.""" @abstractmethod - def device_info(self) -> dict[str, Any] | None: + async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" @abstractmethod - def mac_from_device(self) -> str | None: + async def async_mac_from_device(self) -> str | None: """Try to fetch the mac address of the TV.""" - def is_on(self) -> bool: + @abstractmethod + async def async_get_app_list(self) -> dict[str, str] | None: + """Get installed app list.""" + + async def async_is_on(self) -> bool: """Tells if the TV is on.""" if self._remote is not None: - self.close_remote() + await self.async_close_remote() try: - return self._get_remote() is not None + remote = await self.hass.async_add_executor_job(self._get_remote) + return remote is not None except ( UnhandledResponse, AccessDenied, @@ -139,14 +144,14 @@ def is_on(self) -> bool: # Different reasons, e.g. hostname not resolveable return False - def send_key(self, key: str) -> None: + async def async_send_key(self, key: str, key_type: str | None = None) -> None: """Send a key to the tv and handles exceptions.""" try: # recreate connection if connection was dead retry_count = 1 for _ in range(retry_count + 1): try: - self._send_key(key) + await self._async_send_key(key, key_type) break except ( ConnectionClosed, @@ -164,19 +169,19 @@ def send_key(self, key: str) -> None: pass @abstractmethod - def _send_key(self, key: str) -> None: + async def _async_send_key(self, key: str, key_type: str | None = None) -> None: """Send the key.""" @abstractmethod - def _get_remote(self, avoid_open: bool = False) -> Remote: + def _get_remote(self, avoid_open: bool = False) -> Remote | SamsungTVWS: """Get Remote object.""" - def close_remote(self) -> None: + async def async_close_remote(self) -> None: """Close remote object.""" try: if self._remote is not None: # Close the current remote connection - self._remote.close() + await self.hass.async_add_executor_job(self._remote.close) self._remote = None except OSError: LOGGER.debug("Could not establish connection") @@ -195,9 +200,11 @@ def _notify_new_token_callback(self) -> None: class SamsungTVLegacyBridge(SamsungTVBridge): """The Bridge for Legacy TVs.""" - def __init__(self, method: str, host: str, port: int | None) -> None: + def __init__( + self, hass: HomeAssistant, method: str, host: str, port: int | None + ) -> None: """Initialize Bridge.""" - super().__init__(method, host, LEGACY_PORT) + super().__init__(hass, method, host, LEGACY_PORT) self.config = { CONF_NAME: VALUE_CONF_NAME, CONF_DESCRIPTION: VALUE_CONF_NAME, @@ -208,11 +215,19 @@ def __init__(self, method: str, host: str, port: int | None) -> None: CONF_TIMEOUT: 1, } - def mac_from_device(self) -> None: + async def async_mac_from_device(self) -> None: """Try to fetch the mac address of the TV.""" return None - def try_connect(self) -> str: + async def async_get_app_list(self) -> dict[str, str]: + """Get installed app list.""" + return {} + + async def async_try_connect(self) -> str: + """Try to connect to the Legacy TV.""" + return await self.hass.async_add_executor_job(self._try_connect) + + def _try_connect(self) -> str: """Try to connect to the Legacy TV.""" config = { CONF_NAME: VALUE_CONF_NAME, @@ -239,7 +254,7 @@ def try_connect(self) -> str: LOGGER.debug("Failing config: %s, error: %s", config, err) return RESULT_CANNOT_CONNECT - def device_info(self) -> None: + async def async_device_info(self) -> None: """Try to gather infos of this device.""" return None @@ -261,33 +276,63 @@ def _get_remote(self, avoid_open: bool = False) -> Remote: pass return self._remote + async def _async_send_key(self, key: str, key_type: str | None = None) -> None: + """Send the key using legacy protocol.""" + return await self.hass.async_add_executor_job(self._send_key, key) + def _send_key(self, key: str) -> None: """Send the key using legacy protocol.""" if remote := self._get_remote(): remote.control(key) - def stop(self) -> None: + async def async_stop(self) -> None: """Stop Bridge.""" LOGGER.debug("Stopping SamsungTVLegacyBridge") - self.close_remote() + await self.async_close_remote() class SamsungTVWSBridge(SamsungTVBridge): """The Bridge for WebSocket TVs.""" def __init__( - self, method: str, host: str, port: int | None = None, token: str | None = None + self, + hass: HomeAssistant, + method: str, + host: str, + port: int | None = None, + token: str | None = None, ) -> None: """Initialize Bridge.""" - super().__init__(method, host, port) + super().__init__(hass, method, host, port) self.token = token + self._app_list: dict[str, str] | None = None - def mac_from_device(self) -> str | None: + async def async_mac_from_device(self) -> str | None: """Try to fetch the mac address of the TV.""" - info = self.device_info() + info = await self.async_device_info() return mac_from_device_info(info) if info else None - def try_connect(self) -> str: + async def async_get_app_list(self) -> dict[str, str] | None: + """Get installed app list.""" + return await self.hass.async_add_executor_job(self._get_app_list) + + def _get_app_list(self) -> dict[str, str] | None: + """Get installed app list.""" + if self._app_list is None: + if remote := self._get_remote(): + raw_app_list: list[dict[str, str]] = remote.app_list() + self._app_list = { + app["name"]: app["appId"] + for app in sorted(raw_app_list, key=lambda app: app["name"]) + } + + return self._app_list + + async def async_try_connect(self) -> str: + """Try to connect to the Websocket TV.""" + return await self.hass.async_add_executor_job(self._try_connect) + + def _try_connect(self) -> str: """Try to connect to the Websocket TV.""" for self.port in WEBSOCKET_PORTS: config = { @@ -309,12 +354,12 @@ def try_connect(self) -> str: timeout=config[CONF_TIMEOUT], name=config[CONF_NAME], ) as remote: - remote.open() + remote.open("samsung.remote.control") self.token = remote.token if self.token is None: config[CONF_TOKEN] = "*****" - LOGGER.debug("Working config: %s", config) - return RESULT_SUCCESS + LOGGER.debug("Working config: %s", config) + return RESULT_SUCCESS except WebSocketException as err: LOGGER.debug( "Working but unsupported config: %s, error: %s", config, err @@ -329,23 +374,32 @@ def try_connect(self) -> str: return RESULT_CANNOT_CONNECT - def device_info(self) -> dict[str, Any] | None: + async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" if remote := self._get_remote(avoid_open=True): with contextlib.suppress(HttpApiError, RequestsTimeout): - device_info: dict[str, Any] = remote.rest_device_info() + device_info: dict[str, Any] = await self.hass.async_add_executor_job( + remote.rest_device_info + ) return device_info return None - def _send_key(self, key: str) -> None: + async def _async_send_key(self, key: str, key_type: str | None = None) -> None: + """Send the key using websocket protocol.""" + return await self.hass.async_add_executor_job(self._send_key, key, key_type) + + def _send_key(self, key: str, key_type: str | None = None) -> None: """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" if remote := self._get_remote(): - remote.send_key(key) + if key_type == "run_app": + remote.run_app(key) + else: + remote.send_key(key) - def _get_remote(self, avoid_open: bool = False) -> Remote: + def _get_remote(self, avoid_open: bool = False) -> SamsungTVWS: """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -361,14 +415,19 @@ def _get_remote(self, avoid_open: bool = False) -> Remote: name=VALUE_CONF_NAME, ) if not avoid_open: - self._remote.open() + self._remote.open("samsung.remote.control") # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket - except ConnectionFailure: + except ConnectionFailure as err: + LOGGER.debug("ConnectionFailure %s", err.__repr__()) self._notify_reauth_callback() - except (WebSocketException, OSError): + except (WebSocketException, OSError) as err: + LOGGER.debug("WebSocketException, OSError %s", err.__repr__()) self._remote = None else: + LOGGER.debug( + "Created SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host + ) if self.token != self._remote.token: LOGGER.debug( "SamsungTVWSBridge has provided a new token %s", @@ -378,7 +437,7 @@ def _get_remote(self, avoid_open: bool = False) -> Remote: self._notify_new_token_callback() return self._remote - def stop(self) -> None: + async def async_stop(self) -> None: """Stop Bridge.""" LOGGER.debug("Stopping SamsungTVWSBridge") - self.close_remote() + await self.async_close_remote() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index fe9e43848846f..8b7e0f83a492d 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -124,11 +124,11 @@ def _async_update_and_abort_for_matching_unique_id(self) -> None: updates[CONF_MAC] = self._mac self._abort_if_unique_id_configured(updates=updates) - def _try_connect(self) -> None: + async def _try_connect(self) -> None: """Try to connect and check auth.""" for method in SUPPORTED_METHODS: - self._bridge = SamsungTVBridge.get_bridge(method, self._host) - result = self._bridge.try_connect() + self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) + result = await self._bridge.async_try_connect() if result == RESULT_SUCCESS: return if result != RESULT_CANNOT_CONNECT: @@ -203,7 +203,7 @@ async def async_step_user( """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) - await self.hass.async_add_executor_job(self._try_connect) + await self._try_connect() assert self._bridge self._async_abort_entries_match({CONF_HOST: self._host}) if self._bridge.method != METHOD_LEGACY: @@ -309,7 +309,7 @@ async def async_step_confirm( """Handle user-confirmation of discovered node.""" if user_input is not None: - await self.hass.async_add_executor_job(self._try_connect) + await self._try_connect() assert self._bridge return self._get_entry_from_bridge() @@ -341,9 +341,11 @@ async def async_step_reauth_confirm( assert self._reauth_entry if user_input is not None: bridge = SamsungTVBridge.get_bridge( - self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] + self.hass, + self._reauth_entry.data[CONF_METHOD], + self._reauth_entry.data[CONF_HOST], ) - result = await self.hass.async_add_executor_job(bridge.try_connect) + result = await bridge.async_try_connect() if result == RESULT_SUCCESS: new_data = dict(self._reauth_entry.data) new_data[CONF_TOKEN] = bridge.token diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index 18d2325f38c91..007ab283cfde8 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -1,18 +1,27 @@ """Diagnostics support for SamsungTV.""" from __future__ import annotations +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant +from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge +from .const import DOMAIN + TO_REDACT = {CONF_TOKEN} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" - diag_data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)} - - return diag_data + bridge: SamsungTVLegacyBridge | SamsungTVWSBridge = hass.data[DOMAIN][ + entry.entry_id + ] + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": await bridge.async_device_info(), + } diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9123a68b716f0..21e23c74eb13f 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws==1.6.0", + "samsungtvws==1.7.0", "wakeonlan==2.0.1" ], "ssdp": [ @@ -17,18 +17,22 @@ {"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}} ], "dhcp": [ + {"registered_devices": true}, { "hostname": "tizen*" }, {"macaddress": "8CC8CD*"}, {"macaddress": "606BBD*"}, {"macaddress": "F47B5E*"}, - {"macaddress": "4844F7*"} + {"macaddress": "4844F7*"}, + {"macaddress": "8CEA48*"} ], "codeowners": [ "@escoand", - "@chemelli74" + "@chemelli74", + "@epenet" ], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["samsungctl", "samsungtvws"] } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 7fcc2268d9b16..cb857a96afbe6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -13,6 +13,7 @@ MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -89,6 +90,8 @@ async def async_setup_entry( class SamsungTVDevice(MediaPlayerEntity): """Representation of a Samsung TV.""" + _attr_source_list: list[str] + def __init__( self, bridge: SamsungTVLegacyBridge | SamsungTVWSBridge, @@ -109,6 +112,7 @@ def __init__( self._attr_is_volume_muted: bool = False self._attr_device_class = MediaPlayerDeviceClass.TV self._attr_source_list = list(SOURCES) + self._app_list: dict[str, str] | None = None if self._on_script or self._mac: self._attr_supported_features = SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON @@ -149,21 +153,32 @@ def access_denied(self) -> None: ) ) - def update(self) -> None: + async def async_update(self) -> None: """Update state of device.""" if self._auth_failed or self.hass.is_stopping: return if self._power_off_in_progress(): self._attr_state = STATE_OFF else: - self._attr_state = STATE_ON if self._bridge.is_on() else STATE_OFF + self._attr_state = ( + STATE_ON if await self._bridge.async_is_on() else STATE_OFF + ) + + if self._attr_state == STATE_ON and self._app_list is None: + self._app_list = {} # Ensure that we don't update it twice in parallel + await self._async_update_app_list() + + async def _async_update_app_list(self) -> None: + self._app_list = await self._bridge.async_get_app_list() + if self._app_list is not None: + self._attr_source_list.extend(self._app_list) - def send_key(self, key: str) -> None: + async def _async_send_key(self, key: str, key_type: str | None = None) -> None: """Send a key to the tv and handles exceptions.""" if self._power_off_in_progress() and key != "KEY_POWEROFF": LOGGER.info("TV is powering off, not sending command: %s", key) return - self._bridge.send_key(key) + await self._bridge.async_send_key(key, key_type) def _power_off_in_progress(self) -> bool: return ( @@ -183,55 +198,59 @@ def available(self) -> bool: or self._power_off_in_progress() ) - def turn_off(self) -> None: + async def async_turn_off(self) -> None: """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME - self.send_key("KEY_POWEROFF") + await self._async_send_key("KEY_POWEROFF") # Force closing of remote session to provide instant UI feedback - self._bridge.close_remote() + await self._bridge.async_close_remote() - def volume_up(self) -> None: + async def async_volume_up(self) -> None: """Volume up the media player.""" - self.send_key("KEY_VOLUP") + await self._async_send_key("KEY_VOLUP") - def volume_down(self) -> None: + async def async_volume_down(self) -> None: """Volume down media player.""" - self.send_key("KEY_VOLDOWN") + await self._async_send_key("KEY_VOLDOWN") - def mute_volume(self, mute: bool) -> None: + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" - self.send_key("KEY_MUTE") + await self._async_send_key("KEY_MUTE") - def media_play_pause(self) -> None: + async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._playing: - self.media_pause() + await self.async_media_pause() else: - self.media_play() + await self.async_media_play() - def media_play(self) -> None: + async def async_media_play(self) -> None: """Send play command.""" self._playing = True - self.send_key("KEY_PLAY") + await self._async_send_key("KEY_PLAY") - def media_pause(self) -> None: + async def async_media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - self.send_key("KEY_PAUSE") + await self._async_send_key("KEY_PAUSE") - def media_next_track(self) -> None: + async def async_media_next_track(self) -> None: """Send next track command.""" - self.send_key("KEY_CHUP") + await self._async_send_key("KEY_CHUP") - def media_previous_track(self) -> None: + async def async_media_previous_track(self) -> None: """Send the previous track command.""" - self.send_key("KEY_CHDOWN") + await self._async_send_key("KEY_CHDOWN") async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Support changing a channel.""" + if media_type == MEDIA_TYPE_APP: + await self._async_send_key(media_id, "run_app") + return + if media_type != MEDIA_TYPE_CHANNEL: LOGGER.error("Unsupported media type") return @@ -244,9 +263,9 @@ async def async_play_media( return for digit in media_id: - await self.hass.async_add_executor_job(self.send_key, f"KEY_{digit}") - await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) - await self.hass.async_add_executor_job(self.send_key, "KEY_ENTER") + await self._async_send_key(f"KEY_{digit}") + await asyncio.sleep(KEY_PRESS_TIMEOUT) + await self._async_send_key("KEY_ENTER") def _wake_on_lan(self) -> None: """Wake the device via wake on lan.""" @@ -262,10 +281,14 @@ async def async_turn_on(self) -> None: elif self._mac: await self.hass.async_add_executor_job(self._wake_on_lan) - def select_source(self, source: str) -> None: + async def async_select_source(self, source: str) -> None: """Select input source.""" - if source not in SOURCES: - LOGGER.error("Unsupported source") + if self._app_list and source in self._app_list: + await self._async_send_key(self._app_list[source], "run_app") + return + + if source in SOURCES: + await self._async_send_key(SOURCES[source]) return - self.send_key(SOURCES[source]) + LOGGER.error("Unsupported source") diff --git a/homeassistant/components/samsungtv/translations/el.json b/homeassistant/components/samsungtv/translations/el.json index b9ea5d85166c1..aa2ec61978bd5 100644 --- a/homeassistant/components/samsungtv/translations/el.json +++ b/homeassistant/components/samsungtv/translations/el.json @@ -1,9 +1,18 @@ { "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", "auth_missing": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Samsung. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant.", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "id_missing": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 SerialNumber.", - "not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae." + "missing_config_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2.", + "not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "auth_missing": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Samsung. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant." }, "flow_title": "{device}", "step": { @@ -13,6 +22,13 @@ }, "reauth_confirm": { "description": "\u039c\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7 {device} \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c4\u03cc\u03c2 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03c9\u03bd." + }, + "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" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 Samsung. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c0\u03c1\u03b9\u03bd \u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7." } } } diff --git a/homeassistant/components/samsungtv/translations/pt-BR.json b/homeassistant/components/samsungtv/translations/pt-BR.json new file mode 100644 index 0000000000000..407e9d94d0a5c --- /dev/null +++ b/homeassistant/components/samsungtv/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "auth_missing": "O Home Assistant n\u00e3o est\u00e1 autorizado a se conectar a esta TV Samsung. Verifique as configura\u00e7\u00f5es do Gerenciador de dispositivos externos da sua TV para autorizar o Home Assistant.", + "cannot_connect": "Falha ao conectar", + "id_missing": "Este dispositivo Samsung n\u00e3o possui um SerialNumber.", + "missing_config_entry": "Este dispositivo Samsung n\u00e3o tem uma entrada de configura\u00e7\u00e3o.", + "not_supported": "Este dispositivo Samsung n\u00e3o \u00e9 compat\u00edvel no momento.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "auth_missing": "O Home Assistant n\u00e3o est\u00e1 autorizado a se conectar a esta TV Samsung. Verifique as configura\u00e7\u00f5es do Gerenciador de dispositivos externos da sua TV para autorizar o Home Assistant." + }, + "flow_title": "{device}", + "step": { + "confirm": { + "description": "Deseja configurar {device}? Se voc\u00ea nunca conectou o Home Assistant antes, aparecer\u00e1 um pop-up na sua TV pedindo autoriza\u00e7\u00e3o.", + "title": "TV Samsung" + }, + "reauth_confirm": { + "description": "Ap\u00f3s o envio, aceite o pop-up em {device} solicitando autoriza\u00e7\u00e3o em 30 segundos." + }, + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Insira suas informa\u00e7\u00f5es da Samsung TV. Se voc\u00ea nunca conectou o Home Assistant antes de ver um pop-up na sua TV pedindo autoriza\u00e7\u00e3o." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/sk.json b/homeassistant/components/samsungtv/translations/sk.json new file mode 100644 index 0000000000000..d4a3e2e9fbb3c --- /dev/null +++ b/homeassistant/components/samsungtv/translations/sk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 6aacb3015e1a6..6c4a391698b27 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": ["satel_integra==0.3.4"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["satel_integra"] } diff --git a/homeassistant/components/schluter/manifest.json b/homeassistant/components/schluter/manifest.json index 86f0974b6d12f..90e69afed183a 100644 --- a/homeassistant/components/schluter/manifest.json +++ b/homeassistant/components/schluter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/schluter", "requirements": ["py-schluter==0.1.7"], "codeowners": ["@prairieapps"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["schluter"] } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index c0523e4cbe253..8f2a672ef06ab 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from bs4 import BeautifulSoup import httpx @@ -11,7 +12,7 @@ from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, SensorEntity, ) @@ -33,6 +34,7 @@ from homeassistant.exceptions import PlatformNotReady 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 _LOGGER = logging.getLogger(__name__) @@ -44,7 +46,7 @@ DEFAULT_NAME = "Web scrape" DEFAULT_VERIFY_SSL = True -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, @@ -73,32 +75,32 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - name = config.get(CONF_NAME) - resource = config.get(CONF_RESOURCE) - method = "GET" - payload = None - headers = config.get(CONF_HEADERS) - verify_ssl = config.get(CONF_VERIFY_SSL) - select = config.get(CONF_SELECT) - attr = config.get(CONF_ATTR) - index = config.get(CONF_INDEX) - unit = config.get(CONF_UNIT_OF_MEASUREMENT) - device_class = config.get(CONF_DEVICE_CLASS) - state_class = config.get(CONF_STATE_CLASS) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: + name: str = config[CONF_NAME] + resource: str = config[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: 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) + username: str | None = config.get(CONF_USERNAME) + password: str | None = config.get(CONF_PASSWORD) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: value_template.hass = hass - auth: httpx.DigestAuth | tuple[str, str] | None + 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) - else: - auth = None + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) await rest.async_update() @@ -128,19 +130,19 @@ class ScrapeSensor(SensorEntity): def __init__( self, - rest, - name, - select, - attr, - index, - value_template, - unit, - device_class, - state_class, - ): + rest: RestData, + name: str, + select: str | None, + 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.""" self.rest = rest - self._state = None + self._attr_native_value = None self._select = select self._attr = attr self._index = index @@ -150,12 +152,7 @@ def __init__( self._attr_device_class = device_class self._attr_state_class = state_class - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - def _extract_value(self): + def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" raw_data = BeautifulSoup(self.rest.data, "html.parser") _LOGGER.debug(raw_data) @@ -180,30 +177,26 @@ def _extract_value(self): _LOGGER.debug(value) return value - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the source and updates the state.""" await self.rest.async_update() await self._async_update_from_rest_data() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Ensure the data from the initial update is reflected in the state.""" await self._async_update_from_rest_data() - async def _async_update_from_rest_data(self): + async def _async_update_from_rest_data(self) -> None: """Update state from the rest data.""" if self.rest.data is None: _LOGGER.error("Unable to retrieve data for %s", self.name) return - try: - value = await self.hass.async_add_executor_job(self._extract_value) - except IndexError: - _LOGGER.error("Unable to extract data from HTML for %s", self.name) - return + value = await self.hass.async_add_executor_job(self._extract_value) if self._value_template is not None: - self._state = self._value_template.async_render_with_possible_json_value( - value, None + self._attr_native_value = ( + self._value_template.async_render_with_possible_json_value(value, None) ) else: - self._state = value + self._attr_native_value = value diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index b5f16574f1a01..8580ce8f8fcf5 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -92,7 +92,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 09313dab0dd19..98129e24f0146 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -6,10 +6,12 @@ "requirements": ["screenlogicpy==0.5.4"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ + {"registered_devices": true}, { "hostname": "pentair: *", "macaddress": "00C033*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["screenlogicpy"] } diff --git a/homeassistant/components/screenlogic/translations/pt-BR.json b/homeassistant/components/screenlogic/translations/pt-BR.json new file mode 100644 index 0000000000000..ee99fa03406d2 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/pt-BR.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "description": "Insira as informa\u00e7\u00f5es do seu ScreenLogic Gateway.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "Os seguintes gateways ScreenLogic foram descobertos. Selecione um para configurar ou opte por configurar manualmente um gateway ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre escaneamentos" + }, + "description": "Especifique as configura\u00e7\u00f5es para {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/sk.json b/homeassistant/components/screenlogic/translations/sk.json new file mode 100644 index 0000000000000..f547d5e3a90c0 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "gateway_entry": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index 8720dfac8797b..a9a63ccd9f492 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scsgate", "requirements": ["scsgate==0.1.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["scsgate"] } diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index b48a148034bd6..cfe04f9b1f719 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -5,5 +5,6 @@ "requirements": ["ephem==3.7.7.0"], "codeowners": [], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ephem"] } diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 197654f489f93..23b50c0939f71 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,13 +1,16 @@ """Support for tracking which astronomical or meteorological season it is.""" from __future__ import annotations -from datetime import datetime +from datetime import date, datetime import logging import ephem import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -49,7 +52,7 @@ } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -64,8 +67,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Display the current season.""" - _type = config.get(CONF_TYPE) - name = config.get(CONF_NAME) + _type: str = config[CONF_TYPE] + name: str = config[CONF_NAME] if hass.config.latitude < 0: hemisphere = SOUTHERN @@ -75,33 +78,35 @@ def setup_platform( hemisphere = EQUATOR _LOGGER.debug(_type) - add_entities([Season(hass, hemisphere, _type, name)], True) + add_entities([Season(hemisphere, _type, name)], True) -def get_season(date, hemisphere, season_tracking_type): +def get_season( + current_date: date, hemisphere: str, season_tracking_type: str +) -> str | None: """Calculate the current season.""" if hemisphere == "equator": return None if season_tracking_type == TYPE_ASTRONOMICAL: - spring_start = ephem.next_equinox(str(date.year)).datetime() - summer_start = ephem.next_solstice(str(date.year)).datetime() + spring_start = ephem.next_equinox(str(current_date.year)).datetime() + summer_start = ephem.next_solstice(str(current_date.year)).datetime() autumn_start = ephem.next_equinox(spring_start).datetime() winter_start = ephem.next_solstice(summer_start).datetime() else: - spring_start = datetime(2017, 3, 1).replace(year=date.year) + spring_start = datetime(2017, 3, 1).replace(year=current_date.year) summer_start = spring_start.replace(month=6) autumn_start = spring_start.replace(month=9) winter_start = spring_start.replace(month=12) - if spring_start <= date < summer_start: + if spring_start <= current_date < summer_start: season = STATE_SPRING - elif summer_start <= date < autumn_start: + elif summer_start <= current_date < autumn_start: season = STATE_SUMMER - elif autumn_start <= date < winter_start: + elif autumn_start <= current_date < winter_start: season = STATE_AUTUMN - elif winter_start <= date or spring_start > date: + elif winter_start <= current_date or spring_start > current_date: season = STATE_WINTER # If user is located in the southern hemisphere swap the season @@ -113,36 +118,20 @@ def get_season(date, hemisphere, season_tracking_type): class Season(SensorEntity): """Representation of the current season.""" - def __init__(self, hass, hemisphere, season_tracking_type, name): + _attr_device_class = "season__season" + + def __init__(self, hemisphere: str, season_tracking_type: str, name: str) -> None: """Initialize the season.""" - self.hass = hass - self._name = name + self._attr_name = name self.hemisphere = hemisphere - self.datetime = None self.type = season_tracking_type - self.season = None - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def native_value(self): - """Return the current season.""" - return self.season - @property - def device_class(self): - """Return the device class.""" - return "season__season" - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return SEASON_ICONS.get(self.season, "mdi:cloud") - - def update(self): + def update(self) -> None: """Update season.""" - self.datetime = utcnow().replace(tzinfo=None) - self.season = get_season(self.datetime, self.hemisphere, self.type) + self._attr_native_value = get_season( + utcnow().replace(tzinfo=None), self.hemisphere, self.type + ) + + self._attr_icon = "mdi:cloud" + if self._attr_native_value: + self._attr_icon = SEASON_ICONS[self._attr_native_value] diff --git a/homeassistant/components/select/translations/el.json b/homeassistant/components/select/translations/el.json new file mode 100644 index 0000000000000..8a43dab968159 --- /dev/null +++ b/homeassistant/components/select/translations/el.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2 {entity_name}" + }, + "condition_type": { + "selected_option": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae {entity_name}" + }, + "trigger_type": { + "current_option_changed": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5" + } + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae" +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/pt-BR.json b/homeassistant/components/select/translations/pt-BR.json new file mode 100644 index 0000000000000..1eabd618ce31a --- /dev/null +++ b/homeassistant/components/select/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Alterar op\u00e7\u00e3o de {entity_name}" + }, + "condition_type": { + "selected_option": "Op\u00e7\u00e3o selecionada atual de {entity_name}" + }, + "trigger_type": { + "current_option_changed": "{entity_name} op\u00e7\u00e3o alterada" + } + }, + "title": "Selecionar" +} \ No newline at end of file diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index d31feb5a8e4af..db9a5c9c48a43 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sendgrid", "requirements": ["sendgrid==6.8.2"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["sendgrid"] } diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index edc5cd0823e01..aaf3630ae1946 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -3,18 +3,21 @@ from datetime import timedelta import logging -from sense_energy import ASyncSenseable, SenseAuthenticationException +from sense_energy import ( + ASyncSenseable, + SenseAuthenticationException, + SenseMFARequiredException, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EMAIL, - CONF_PASSWORD, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import HomeAssistant, callback -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.event import async_track_time_interval @@ -58,9 +61,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data = entry.data email = entry_data[CONF_EMAIL] - password = entry_data[CONF_PASSWORD] timeout = entry_data[CONF_TIMEOUT] + access_token = entry_data.get("access_token", "") + user_id = entry_data.get("user_id", "") + monitor_id = entry_data.get("monitor_id", "") + client_session = async_get_clientsession(hass) gateway = ASyncSenseable( @@ -69,16 +75,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway.rate_limit = ACTIVE_UPDATE_RATE try: - await gateway.authenticate(email, password) - except SenseAuthenticationException: - _LOGGER.error("Could not authenticate with sense server") - return False - except SENSE_TIMEOUT_EXCEPTIONS as err: - raise ConfigEntryNotReady( - str(err) or "Timed out during authentication" - ) from err - except SENSE_EXCEPTIONS as err: - raise ConfigEntryNotReady(str(err) or "Error during authentication") from err + gateway.load_auth(access_token, user_id, monitor_id) + await gateway.get_monitor_data() + except (SenseAuthenticationException, SenseMFARequiredException) as err: + _LOGGER.warning("Sense authentication expired") + raise ConfigEntryAuthFailed(err) from err sense_devices_data = SenseDevicesData() try: @@ -91,11 +92,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SENSE_EXCEPTIONS as err: raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err + async def _async_update_trend(): + """Update the trend data.""" + try: + await gateway.update_trend_data() + except (SenseAuthenticationException, SenseMFARequiredException) as err: + _LOGGER.warning("Sense authentication expired") + raise ConfigEntryAuthFailed(err) from err + trends_coordinator: DataUpdateCoordinator[None] = DataUpdateCoordinator( hass, _LOGGER, name=f"Sense Trends {email}", - update_method=gateway.update_trend_data, + update_method=_async_update_trend, update_interval=timedelta(seconds=300), ) # Start out as unavailable so we do not report 0 data diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 6bd33291d7f5c..eea3642466244 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Sense integration.""" import logging -from sense_energy import ASyncSenseable, SenseAuthenticationException +from sense_energy import ( + ASyncSenseable, + SenseAuthenticationException, + SenseMFARequiredException, +) import voluptuous as vol -from homeassistant import config_entries, core -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant import config_entries +from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS @@ -21,37 +25,74 @@ ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sense.""" - Data has the keys from DATA_SCHEMA with values provided by the user. - """ - timeout = data[CONF_TIMEOUT] - client_session = async_get_clientsession(hass) + VERSION = 1 - gateway = ASyncSenseable( - api_timeout=timeout, wss_timeout=timeout, client_session=client_session - ) - gateway.rate_limit = ACTIVE_UPDATE_RATE - await gateway.authenticate(data[CONF_EMAIL], data[CONF_PASSWORD]) + def __init__(self): + """Init Config .""" + self._gateway = None + self._auth_data = {} + super().__init__() - # Return info that you want to store in the config entry. - return {"title": data[CONF_EMAIL]} + async def validate_input(self, data): + """Validate the user input allows us to connect. + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + self._auth_data.update(dict(data)) + timeout = self._auth_data[CONF_TIMEOUT] + client_session = async_get_clientsession(self.hass) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Sense.""" + self._gateway = ASyncSenseable( + api_timeout=timeout, wss_timeout=timeout, client_session=client_session + ) + self._gateway.rate_limit = ACTIVE_UPDATE_RATE + await self._gateway.authenticate( + self._auth_data[CONF_EMAIL], self._auth_data[CONF_PASSWORD] + ) - VERSION = 1 + async def create_entry_from_data(self): + """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["monitor_id"] = self._gateway.sense_monitor_id + existing_entry = await self.async_set_unique_id(self._auth_data[CONF_EMAIL]) + if not existing_entry: + return self.async_create_entry( + title=self._auth_data[CONF_EMAIL], data=self._auth_data + ) - async def async_step_user(self, user_input=None): - """Handle the initial step.""" + self.hass.config_entries.async_update_entry( + existing_entry, data=self._auth_data + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + async def validate_input_and_create_entry(self, user_input, errors): + """Validate the input and create the entry from the data.""" + try: + await self.validate_input(user_input) + except SenseMFARequiredException: + return await self.async_step_validation() + except SENSE_TIMEOUT_EXCEPTIONS: + errors["base"] = "cannot_connect" + except SenseAuthenticationException: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return await self.create_entry_from_data() + return None + + async def async_step_validation(self, user_input=None): + """Handle validation (2fa) step.""" errors = {} - if user_input is not None: + if user_input: try: - info = await validate_input(self.hass, user_input) - await self.async_set_unique_id(user_input[CONF_EMAIL]) - return self.async_create_entry(title=info["title"], data=user_input) + await self._gateway.validate_mfa(user_input[CONF_CODE]) except SENSE_TIMEOUT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: @@ -59,7 +100,43 @@ async def async_step_user(self, user_input=None): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + else: + return await self.create_entry_from_data() + + return self.async_show_form( + step_id="validation", + data_schema=vol.Schema({vol.Required(CONF_CODE): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if result := await self.validate_input_and_create_entry(user_input, errors): + return result return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + async def async_step_reauth(self, data): + """Handle configuration by re-auth.""" + self._auth_data = dict(data) + return await self.async_step_reauth_validate(data) + + async def async_step_reauth_validate(self, user_input=None): + """Handle reauth and validation.""" + errors = {} + if user_input is not None: + if result := await self.validate_input_and_create_entry(user_input, errors): + return result + + return self.async_show_form( + step_id="reauth_validate", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_EMAIL: self._auth_data[CONF_EMAIL], + }, + ) diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 361a58379d34a..30de722a7bc60 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.9.6"], + "requirements": ["sense_energy==0.10.2"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ @@ -19,5 +19,6 @@ "macaddress": "A4D578*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["sense_energy"] } diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json index 29e85c98fc240..a519155bee135 100644 --- a/homeassistant/components/sense/strings.json +++ b/homeassistant/components/sense/strings.json @@ -8,6 +8,19 @@ "password": "[%key:common::config_flow::data::password%]", "timeout": "Timeout" } + }, + "validation": { + "title": "Sense Multi-factor authentication", + "data": { + "code": "Verification code" + } + }, + "reauth_validate": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Sense integration needs to re-authenticate your account {email}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -16,7 +29,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/sense/translations/ca.json b/homeassistant/components/sense/translations/ca.json index aff80de710d6a..f852feefb0e73 100644 --- a/homeassistant/components/sense/translations/ca.json +++ b/homeassistant/components/sense/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_validate": { + "data": { + "password": "Contrasenya" + }, + "description": "La integraci\u00f3 Sense ha de tornar a autenticar el compte {email}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "email": "Correu electr\u00f2nic", @@ -16,6 +24,12 @@ "timeout": "Temps d'espera" }, "title": "Connexi\u00f3 amb Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Codi de verificaci\u00f3" + }, + "title": "Autenticaci\u00f3 multi-factor de Sense" } } } diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index d0290abdf981e..5c7002aaa2282 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_validate": { + "data": { + "password": "Passwort" + }, + "description": "Die Sense-Integration muss dein Konto {email} erneut authentifizieren.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "email": "E-Mail", @@ -16,6 +24,12 @@ "timeout": "Zeit\u00fcberschreitung" }, "title": "Stelle eine Verbindung zu deinem Sense Energy Monitor her" + }, + "validation": { + "data": { + "code": "Verifizierungs-Code" + }, + "title": "Sense Multi-Faktor-Authentifizierung" } } } diff --git a/homeassistant/components/sense/translations/el.json b/homeassistant/components/sense/translations/el.json index 1fa97f4210502..b70057311722a 100644 --- a/homeassistant/components/sense/translations/el.json +++ b/homeassistant/components/sense/translations/el.json @@ -1,8 +1,35 @@ { "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", + "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", + "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": { + "reauth_validate": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Sense \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 {email} .", + "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": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf" + }, "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2" + }, + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd Sense" } } } diff --git a/homeassistant/components/sense/translations/en.json b/homeassistant/components/sense/translations/en.json index 24cde7411a890..fd9a7ade4bfb0 100644 --- a/homeassistant/components/sense/translations/en.json +++ b/homeassistant/components/sense/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "The Sense integration needs to re-authenticate your account {email}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "email": "Email", @@ -16,6 +24,12 @@ "timeout": "Timeout" }, "title": "Connect to your Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Verification code" + }, + "title": "Sense Multi-factor authentication" } } } diff --git a/homeassistant/components/sense/translations/et.json b/homeassistant/components/sense/translations/et.json index 8438be5c677f1..1d0b64b5054ed 100644 --- a/homeassistant/components/sense/translations/et.json +++ b/homeassistant/components/sense/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "unknown": "Tundmatu viga" }, "step": { + "reauth_validate": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sense'i sidumine peab konto {email} uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "email": "E-post", @@ -16,6 +24,12 @@ "timeout": "Ajal\u00f5pp" }, "title": "\u00dchendu oma Sense Energy Monitor'iga" + }, + "validation": { + "data": { + "code": "Kinnituskood" + }, + "title": "Sense mitmeastmeline autentimine" } } } diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json index 9defa2971bb50..28dc6dbb00c29 100644 --- a/homeassistant/components/sense/translations/hu.json +++ b/homeassistant/components/sense/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_validate": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A Sense integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie fi\u00f3kj\u00e1t {email} .", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "email": "E-mail", @@ -16,6 +24,12 @@ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s" }, "title": "Csatlakoztassa a Sense Energy Monitort" + }, + "validation": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d" + }, + "title": "Sense t\u00f6bbfaktoros hiteles\u00edt\u00e9s" } } } diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 60fe4c88e2006..437ce96d9f19e 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\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" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -9,6 +10,13 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_validate": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "Sense\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "email": "E\u30e1\u30fc\u30eb", @@ -16,6 +24,12 @@ "timeout": "\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "title": "Sense Energy Monitor\u306b\u63a5\u7d9a\u3059\u308b" + }, + "validation": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, + "title": "Sense\u591a\u8981\u7d20\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/sense/translations/no.json b/homeassistant/components/sense/translations/no.json index 11f92bfccb462..004580b51926e 100644 --- a/homeassistant/components/sense/translations/no.json +++ b/homeassistant/components/sense/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_validate": { + "data": { + "password": "Passord" + }, + "description": "Sense-integrasjonen m\u00e5 autentisere kontoen din {email} p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "email": "E-post", @@ -16,6 +24,12 @@ "timeout": "Tidsavbrudd" }, "title": "Koble til din Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Bekreftelseskode" + }, + "title": "Sense multi-faktor autentisering" } } } diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json index 8bc58118a232b..86dcb69f4c669 100644 --- a/homeassistant/components/sense/translations/pl.json +++ b/homeassistant/components/sense/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_validate": { + "data": { + "password": "Has\u0142o" + }, + "description": "Integracja Sense wymaga ponownego uwierzytelnienia Twojego konta {email}.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "email": "Adres e-mail", @@ -16,6 +24,12 @@ "timeout": "Limit czasu" }, "title": "Po\u0142\u0105czenie z monitorem energii Sense" + }, + "validation": { + "data": { + "code": "Kod weryfikacyjny" + }, + "title": "Uwierzytelnianie wielosk\u0142adnikowe Sense" } } } diff --git a/homeassistant/components/sense/translations/pt-BR.json b/homeassistant/components/sense/translations/pt-BR.json index b61651bf441f2..5944daf63caf3 100644 --- a/homeassistant/components/sense/translations/pt-BR.json +++ b/homeassistant/components/sense/translations/pt-BR.json @@ -1,18 +1,35 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "reauth_validate": { + "data": { + "password": "Senha" + }, + "description": "A integra\u00e7\u00e3o do Sense precisa autenticar novamente sua conta {email} .", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { + "email": "Email", + "password": "Senha", "timeout": "Tempo limite" - } + }, + "title": "Conecte-se ao seu monitor de Energia Sense" + }, + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + }, + "title": "Sense autentica\u00e7\u00e3o multifator" } } } diff --git a/homeassistant/components/sense/translations/ru.json b/homeassistant/components/sense/translations/ru.json index c113c06a0218b..9b14769b83971 100644 --- a/homeassistant/components/sense/translations/ru.json +++ b/homeassistant/components/sense/translations/ru.json @@ -1,7 +1,8 @@ { "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_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.", + "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.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Sense {email}.", + "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": { "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", @@ -16,6 +24,12 @@ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442" }, "title": "Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "title": "\u041c\u043d\u043e\u0433\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f Sense" } } } diff --git a/homeassistant/components/sense/translations/sk.json b/homeassistant/components/sense/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/sense/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index 5ca9a9f847d91..720db0d1bd8d9 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_validate": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "Sense \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f {email}\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", @@ -16,6 +24,12 @@ "timeout": "\u903e\u6642" }, "title": "\u9023\u7dda\u81f3 Sense \u80fd\u6e90\u76e3\u63a7" + }, + "validation": { + "data": { + "code": "\u9a57\u8b49\u78bc" + }, + "title": "Sense \u591a\u6b65\u9a5f\u9a57\u8b49" } } } diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index d8e607ec816c8..78f6e0609bcfa 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sensehat", "requirements": ["sense-hat==2.2.0"], "codeowners": [], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["sense_hat"] } diff --git a/homeassistant/components/senseme/config_flow.py b/homeassistant/components/senseme/config_flow.py index 151a251c8a2c5..6e2f10c1b36e2 100644 --- a/homeassistant/components/senseme/config_flow.py +++ b/homeassistant/components/senseme/config_flow.py @@ -42,10 +42,10 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes self._discovered_device = device return await self.async_step_discovery_confirm() - async def async_step_discovery( + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" + """Handle integration discovery.""" uuid = discovery_info[CONF_ID] device = async_get_discovered_device(self.hass, discovery_info[CONF_ID]) host = device.address diff --git a/homeassistant/components/senseme/discovery.py b/homeassistant/components/senseme/discovery.py index 85674f069e12a..624b18a8761fb 100644 --- a/homeassistant/components/senseme/discovery.py +++ b/homeassistant/components/senseme/discovery.py @@ -58,7 +58,7 @@ def async_trigger_discovery( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: device.uuid}, ) ) diff --git a/homeassistant/components/senseme/manifest.json b/homeassistant/components/senseme/manifest.json index 7eba9eb4bdad4..97a73434b266e 100644 --- a/homeassistant/components/senseme/manifest.json +++ b/homeassistant/components/senseme/manifest.json @@ -9,6 +9,10 @@ "codeowners": [ "@mikelawrence", "@bdraco" ], - "dhcp": [{"macaddress":"20F85E*"}], - "iot_class": "local_push" + "dhcp": [ + {"registered_devices": true}, + {"macaddress":"20F85E*"} + ], + "iot_class": "local_push", + "loggers": ["aiosenseme"] } diff --git a/homeassistant/components/senseme/translations/bg.json b/homeassistant/components/senseme/translations/bg.json index a01a685bf0cb4..4ae9d109df4d1 100644 --- a/homeassistant/components/senseme/translations/bg.json +++ b/homeassistant/components/senseme/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" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -9,6 +10,9 @@ }, "flow_title": "{name} - {model} ({host})", "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} - {model} ({host})?" + }, "manual": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/senseme/translations/ca.json b/homeassistant/components/senseme/translations/ca.json index ed36968e61568..6eccc55fa5281 100644 --- a/homeassistant/components/senseme/translations/ca.json +++ b/homeassistant/components/senseme/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/senseme/translations/de.json b/homeassistant/components/senseme/translations/de.json index afacd480ce122..01463118ec09e 100644 --- a/homeassistant/components/senseme/translations/de.json +++ b/homeassistant/components/senseme/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/senseme/translations/el.json b/homeassistant/components/senseme/translations/el.json index 152051868bc5a..f19dd73dde963 100644 --- a/homeassistant/components/senseme/translations/el.json +++ b/homeassistant/components/senseme/translations/el.json @@ -1,11 +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", + "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", + "invalid_host": "\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" + }, "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} - {model} ({host});" }, "manual": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP." }, "user": { diff --git a/homeassistant/components/senseme/translations/fr.json b/homeassistant/components/senseme/translations/fr.json index d8f772f9e513a..efa8292519187 100644 --- a/homeassistant/components/senseme/translations/fr.json +++ b/homeassistant/components/senseme/translations/fr.json @@ -1,12 +1,14 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" }, "error": { "cannot_connect": "Impossible de se connecter", "invalid_host": "Adresse IP ou nom d'h\u00f4te invalide" }, + "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { "description": "Voulez-vous configurer {name} - {model} ( {host} )\u00a0?" diff --git a/homeassistant/components/senseme/translations/he.json b/homeassistant/components/senseme/translations/he.json index 55eb3a0d66003..3b3e7dea4bfe0 100644 --- a/homeassistant/components/senseme/translations/he.json +++ b/homeassistant/components/senseme/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", + "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", diff --git a/homeassistant/components/senseme/translations/hu.json b/homeassistant/components/senseme/translations/hu.json index 06299000122c9..1aed50994123a 100644 --- a/homeassistant/components/senseme/translations/hu.json +++ b/homeassistant/components/senseme/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/senseme/translations/id.json b/homeassistant/components/senseme/translations/id.json index f7c3d821e661b..9aef5e20dbd5a 100644 --- a/homeassistant/components/senseme/translations/id.json +++ b/homeassistant/components/senseme/translations/id.json @@ -1,17 +1,29 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_host": "Nama host atau alamat IP tidak valid" }, + "flow_title": "{name} - {model} ({host})", "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {name} - {model} ({host})?" + }, "manual": { "data": { "host": "Host" - } + }, + "description": "Masukkan Alamat IP." + }, + "user": { + "data": { + "device": "Perangkat" + }, + "description": "Pilih perangkat, atau pilih 'Alamat IP' untuk memasukkan Alamat IP secara manual." } } } diff --git a/homeassistant/components/senseme/translations/it.json b/homeassistant/components/senseme/translations/it.json index 5378ff71ef131..dcdb8bcd72862 100644 --- a/homeassistant/components/senseme/translations/it.json +++ b/homeassistant/components/senseme/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/senseme/translations/ja.json b/homeassistant/components/senseme/translations/ja.json index 4971b9f4494dc..36fd50cfdcc60 100644 --- a/homeassistant/components/senseme/translations/ja.json +++ b/homeassistant/components/senseme/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/senseme/translations/lv.json b/homeassistant/components/senseme/translations/lv.json new file mode 100644 index 0000000000000..35d9add569f70 --- /dev/null +++ b/homeassistant/components/senseme/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "Ier\u012bce" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senseme/translations/nl.json b/homeassistant/components/senseme/translations/nl.json index bc058d00b60d4..6cb73c4c9f2f9 100644 --- a/homeassistant/components/senseme/translations/nl.json +++ b/homeassistant/components/senseme/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,9 @@ }, "flow_title": "{name} - {model} ({host})", "step": { + "discovery_confirm": { + "description": "Wilt u {name} - {model} ({host})?" + }, "manual": { "data": { "host": "Host" diff --git a/homeassistant/components/senseme/translations/no.json b/homeassistant/components/senseme/translations/no.json index 6895aeb247a41..47e330100c91b 100644 --- a/homeassistant/components/senseme/translations/no.json +++ b/homeassistant/components/senseme/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/senseme/translations/pl.json b/homeassistant/components/senseme/translations/pl.json index f348b97d11123..a5fb61a685b77 100644 --- a/homeassistant/components/senseme/translations/pl.json +++ b/homeassistant/components/senseme/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "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", diff --git a/homeassistant/components/senseme/translations/pt-BR.json b/homeassistant/components/senseme/translations/pt-BR.json new file mode 100644 index 0000000000000..210b33376d504 --- /dev/null +++ b/homeassistant/components/senseme/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido" + }, + "flow_title": "{name} - {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar {name} - {model} ( {host} )?" + }, + "manual": { + "data": { + "host": "Nome do host" + }, + "description": "Digite um endere\u00e7o IP." + }, + "user": { + "data": { + "device": "Dispositivo" + }, + "description": "Selecione um dispositivo ou escolha 'Endere\u00e7o IP' para inserir manualmente um endere\u00e7o IP." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senseme/translations/ru.json b/homeassistant/components/senseme/translations/ru.json index 725169c668a34..8debd33481f53 100644 --- a/homeassistant/components/senseme/translations/ru.json +++ b/homeassistant/components/senseme/translations/ru.json @@ -1,7 +1,8 @@ { "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_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.", diff --git a/homeassistant/components/senseme/translations/sv.json b/homeassistant/components/senseme/translations/sv.json new file mode 100644 index 0000000000000..46631acc69a79 --- /dev/null +++ b/homeassistant/components/senseme/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senseme/translations/tr.json b/homeassistant/components/senseme/translations/tr.json index 6d316999cb51d..87c43a0832629 100644 --- a/homeassistant/components/senseme/translations/tr.json +++ b/homeassistant/components/senseme/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/senseme/translations/zh-Hant.json b/homeassistant/components/senseme/translations/zh-Hant.json index 56af531b9d54d..9875ffa97c241 100644 --- a/homeassistant/components/senseme/translations/zh-Hant.json +++ b/homeassistant/components/senseme/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index 7401a8c2150e8..b62482b60b5fa 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1,53 +1,22 @@ """The sensibo component.""" from __future__ import annotations -import asyncio -import logging - -import aiohttp -import async_timeout -import pysensibo - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import _INITIAL_FETCH_FIELDS, DOMAIN, PLATFORMS, TIMEOUT -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORMS +from .coordinator import SensiboDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sensibo from a config entry.""" - client = pysensibo.SensiboClient( - entry.data[CONF_API_KEY], session=async_get_clientsession(hass), timeout=TIMEOUT - ) - devices = [] - try: - async with async_timeout.timeout(TIMEOUT): - for dev in await client.async_get_devices(_INITIAL_FETCH_FIELDS): - devices.append(dev) - except ( - aiohttp.client_exceptions.ClientConnectorError, - asyncio.TimeoutError, - pysensibo.SensiboError, - ) as err: - raise ConfigEntryNotReady( - f"Failed to get devices from Sensibo servers: {err}" - ) from err - - if not devices: - return False - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "devices": devices, - "client": client, - } + + coordinator = SensiboDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) - _LOGGER.debug("Loaded entry for %s", entry.title) + return True @@ -57,6 +26,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: del hass.data[DOMAIN] - _LOGGER.debug("Unloaded entry for %s", entry.title) return True return False diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 648e05dbe797f..f829fd9ed3959 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -2,12 +2,10 @@ from __future__ import annotations import asyncio -import logging -from typing import Any -import aiohttp +from aiohttp.client_exceptions import ClientConnectionError import async_timeout -from pysensibo import SensiboClient, SensiboError +from pysensibo.exceptions import AuthenticationError, SensiboError import voluptuous as vol from homeassistant.components.climate import ( @@ -27,25 +25,25 @@ ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, - STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.temperature import convert as convert_temperature -from .const import _FETCH_FIELDS, ALL, DOMAIN, TIMEOUT - -_LOGGER = logging.getLogger(__name__) +from .const import ALL, DOMAIN, LOGGER, TIMEOUT +from .coordinator import SensiboDataUpdateCoordinator SERVICE_ASSUME_STATE = "assume_state" @@ -56,10 +54,6 @@ } ) -ASSUME_STATE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string} -) - FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, "swing": SUPPORT_SWING_MODE, @@ -72,11 +66,19 @@ "fan": HVAC_MODE_FAN_ONLY, "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, - "": HVAC_MODE_OFF, + "off": HVAC_MODE_OFF, } HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} +AC_STATE_TO_DATA = { + "targetTemperature": "target_temp", + "fanLevel": "fan_mode", + "on": "on", + "mode": "hvac_mode", + "swing": "swing_mode", +} + async def async_setup_platform( hass: HomeAssistant, @@ -85,7 +87,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up Sensibo devices.""" - _LOGGER.warning( + LOGGER.warning( "Loading Sensibo via platform setup is deprecated; Please remove it from your configuration" ) hass.async_create_task( @@ -102,172 +104,195 @@ async def async_setup_entry( ) -> None: """Set up the Sensibo climate entry.""" - data = hass.data[DOMAIN][entry.entry_id] - client = data["client"] - devices = data["devices"] + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [ - SensiboClimate(client, dev, hass.config.units.temperature_unit) - for dev in devices + SensiboClimate(coordinator, device_id) + for device_id, device_data in coordinator.data.items() + # Remove none climate devices + if device_data["hvac_modes"] and device_data["temp"] ] async_add_entities(entities) - async def async_assume_state(service: ServiceCall) -> None: - """Set state according to external service call..""" - if entity_ids := service.data.get(ATTR_ENTITY_ID): - target_climate = [ - entity for entity in entities if entity.entity_id in entity_ids - ] - else: - target_climate = entities - - update_tasks = [] - for climate in target_climate: - await climate.async_assume_state(service.data.get(ATTR_STATE)) - update_tasks.append(climate.async_update_ha_state(True)) - - if update_tasks: - await asyncio.wait(update_tasks) - - hass.services.async_register( - DOMAIN, + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( SERVICE_ASSUME_STATE, - async_assume_state, - schema=ASSUME_STATE_SCHEMA, + { + vol.Required(ATTR_STATE): vol.In(["on", "off"]), + }, + "async_assume_state", ) -class SensiboClimate(ClimateEntity): +class SensiboClimate(CoordinatorEntity, ClimateEntity): """Representation of a Sensibo device.""" - def __init__(self, client: SensiboClient, data: dict[str, Any], units: str) -> None: + coordinator: SensiboDataUpdateCoordinator + + def __init__( + self, coordinator: SensiboDataUpdateCoordinator, device_id: str + ) -> None: """Initiate SensiboClimate.""" - self._client = client - self._id = data["id"] - self._external_state = None - self._units = units - self._failed_update = False - self._attr_available = False - self._attr_unique_id = self._id + super().__init__(coordinator) + self._client = coordinator.client + self._attr_unique_id = device_id + self._attr_name = coordinator.data[device_id]["name"] self._attr_temperature_unit = ( - TEMP_CELSIUS if data["temperatureUnit"] == "C" else TEMP_FAHRENHEIT - ) - self._do_update(data) - self._attr_target_temperature_step = ( - 1 if self.temperature_unit == units else None + TEMP_CELSIUS + if coordinator.data[device_id]["temp_unit"] == "C" + else TEMP_FAHRENHEIT ) + self._attr_supported_features = self.get_features() self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._id)}, - name=self._attr_name, + identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, + name=coordinator.data[device_id]["name"], + connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", - model=data["productModel"], - sw_version=data["firmwareVersion"], - hw_version=data["firmwareType"], - suggested_area=self._attr_name, + model=coordinator.data[device_id]["model"], + sw_version=coordinator.data[device_id]["fw_ver"], + hw_version=coordinator.data[device_id]["fw_type"], + suggested_area=coordinator.data[device_id]["name"], ) - def _do_update(self, data) -> None: - self._attr_name = data["room"]["name"] - self._ac_states = data["acState"] - self._attr_extra_state_attributes = { - "battery": data["measurements"].get("batteryVoltage") - } - self._attr_current_temperature = convert_temperature( - data["measurements"].get("temperature"), - TEMP_CELSIUS, - self._attr_temperature_unit, - ) - self._attr_current_humidity = data["measurements"].get("humidity") - - self._attr_target_temperature = self._ac_states.get("targetTemperature") - if self._ac_states["on"]: - self._attr_hvac_mode = SENSIBO_TO_HA.get(self._ac_states["mode"], "") - else: - self._attr_hvac_mode = HVAC_MODE_OFF - self._attr_fan_mode = self._ac_states.get("fanLevel") - self._attr_swing_mode = self._ac_states.get("swing") - - self._attr_available = data["connectionStatus"].get("isAlive") - capabilities = data["remoteCapabilities"] - self._attr_hvac_modes = [SENSIBO_TO_HA[mode] for mode in capabilities["modes"]] - self._attr_hvac_modes.append(HVAC_MODE_OFF) - - current_capabilities = capabilities["modes"][self._ac_states.get("mode")] - self._attr_fan_modes = current_capabilities.get("fanLevels") - self._attr_swing_modes = current_capabilities.get("swing") - - temperature_unit_key = data.get("temperatureUnit") or self._ac_states.get( - "temperatureUnit" - ) - if temperature_unit_key: - self._temperature_unit = ( - TEMP_CELSIUS if temperature_unit_key == "C" else TEMP_FAHRENHEIT - ) - self._temperatures_list = ( - current_capabilities["temperatures"] - .get(temperature_unit_key, {}) - .get("values", []) - ) - else: - self._temperature_unit = self._units - self._temperatures_list = [] - self._attr_min_temp = ( - self._temperatures_list[0] if self._temperatures_list else super().min_temp - ) - self._attr_max_temp = ( - self._temperatures_list[-1] if self._temperatures_list else super().max_temp + def get_features(self) -> int: + """Get supported features.""" + features = 0 + for key in self.coordinator.data[self.unique_id]["full_features"]: + if key in FIELD_TO_FLAG: + features |= FIELD_TO_FLAG[key] + return features + + @property + def current_humidity(self) -> int: + """Return the current humidity.""" + return self.coordinator.data[self.unique_id]["humidity"] + + @property + def hvac_mode(self) -> str: + """Return hvac operation.""" + return ( + SENSIBO_TO_HA[self.coordinator.data[self.unique_id]["hvac_mode"]] + if self.coordinator.data[self.unique_id]["on"] + else HVAC_MODE_OFF ) - self._attr_temperature_unit = self._temperature_unit - self._attr_supported_features = 0 - for key in self._ac_states: - if key in FIELD_TO_FLAG: - self._attr_supported_features |= FIELD_TO_FLAG[key] + @property + def hvac_modes(self) -> list[str]: + """Return the list of available hvac operation modes.""" + return [ + SENSIBO_TO_HA[mode] + for mode in self.coordinator.data[self.unique_id]["hvac_modes"] + ] + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return convert_temperature( + self.coordinator.data[self.unique_id]["temp"], + TEMP_CELSIUS, + self.temperature_unit, + ) - self._attr_state = self._external_state or super().state + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self.coordinator.data[self.unique_id]["target_temp"] + + @property + def target_temperature_step(self) -> float | None: + """Return the supported step of target temperature.""" + return self.coordinator.data[self.unique_id]["temp_step"] + + @property + def fan_mode(self) -> str | None: + """Return the fan setting.""" + return self.coordinator.data[self.unique_id]["fan_mode"] + + @property + def fan_modes(self) -> list[str] | None: + """Return the list of available fan modes.""" + return self.coordinator.data[self.unique_id]["fan_modes"] + + @property + def swing_mode(self) -> str | None: + """Return the swing setting.""" + return self.coordinator.data[self.unique_id]["swing_mode"] + + @property + def swing_modes(self) -> list[str] | None: + """Return the list of available swing modes.""" + return self.coordinator.data[self.unique_id]["swing_modes"] + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self.coordinator.data[self.unique_id]["temp_list"][0] + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self.coordinator.data[self.unique_id]["temp_list"][-1] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.data[self.unique_id]["available"] and super().available async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" + if ( + "targetTemperature" + not in self.coordinator.data[self.unique_id]["active_features"] + ): + raise HomeAssistantError( + "Current mode doesn't support setting Target Temperature" + ) + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return - temperature = int(temperature) - if temperature not in self._temperatures_list: + + if temperature == self.target_temperature: + return + + if temperature not in self.coordinator.data[self.unique_id]["temp_list"]: # Requested temperature is not supported. - if temperature == self.target_temperature: - return - index = self._temperatures_list.index(self.target_temperature) - if ( - temperature > self.target_temperature - and index < len(self._temperatures_list) - 1 - ): - temperature = self._temperatures_list[index + 1] - elif temperature < self.target_temperature and index > 0: - temperature = self._temperatures_list[index - 1] + if temperature > self.coordinator.data[self.unique_id]["temp_list"][-1]: + temperature = self.coordinator.data[self.unique_id]["temp_list"][-1] + + elif temperature < self.coordinator.data[self.unique_id]["temp_list"][0]: + temperature = self.coordinator.data[self.unique_id]["temp_list"][0] + else: return - await self._async_set_ac_state_property("targetTemperature", temperature) + await self._async_set_ac_state_property("targetTemperature", int(temperature)) - async def async_set_fan_mode(self, fan_mode) -> None: + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" + if "fanLevel" not in self.coordinator.data[self.unique_id]["active_features"]: + raise HomeAssistantError("Current mode doesn't support setting Fanlevel") + await self._async_set_ac_state_property("fanLevel", fan_mode) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" if hvac_mode == HVAC_MODE_OFF: await self._async_set_ac_state_property("on", False) return # Turn on if not currently on. - if not self._ac_states["on"]: + if not self.coordinator.data[self.unique_id]["on"]: await self._async_set_ac_state_property("on", True) await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) - async def async_set_swing_mode(self, swing_mode) -> None: + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" + if "swing" not in self.coordinator.data[self.unique_id]["active_features"]: + raise HomeAssistantError("Current mode doesn't support setting Swing") + await self._async_set_ac_state_property("swing", swing_mode) async def async_turn_on(self) -> None: @@ -278,68 +303,41 @@ async def async_turn_off(self) -> None: """Turn Sensibo unit on.""" await self._async_set_ac_state_property("on", False) - async def async_assume_state(self, state) -> None: - """Set external state.""" - change_needed = (state != HVAC_MODE_OFF and not self._ac_states["on"]) or ( - state == HVAC_MODE_OFF and self._ac_states["on"] - ) - - if change_needed: - await self._async_set_ac_state_property("on", state != HVAC_MODE_OFF, True) - - if state in (STATE_ON, HVAC_MODE_OFF): - self._external_state = None - else: - self._external_state = state - - async def async_update(self) -> None: - """Retrieve latest state.""" - try: - async with async_timeout.timeout(TIMEOUT): - data = await self._client.async_get_device(self._id, _FETCH_FIELDS) - except ( - aiohttp.client_exceptions.ClientError, - asyncio.TimeoutError, - SensiboError, - ) as err: - if self._failed_update: - _LOGGER.warning( - "Failed to update data for device '%s' from Sensibo servers with error %s", - self._attr_name, - err, - ) - self._attr_available = False - self.async_write_ha_state() - return - - _LOGGER.debug("First failed update data for device '%s'", self._attr_name) - self._failed_update = True - return - - if self.temperature_unit == self.hass.config.units.temperature_unit: - self._attr_target_temperature_step = 1 - else: - self._attr_target_temperature_step = None - - self._failed_update = False - self._do_update(data) - async def _async_set_ac_state_property( - self, name, value, assumed_state=False + self, name: str, value: str | int | bool, assumed_state: bool = False ) -> None: """Set AC state.""" + result = {} try: async with async_timeout.timeout(TIMEOUT): - await self._client.async_set_ac_state_property( - self._id, name, value, self._ac_states, assumed_state + result = await self._client.async_set_ac_state_property( + self.unique_id, + name, + value, + self.coordinator.data[self.unique_id]["ac_states"], + assumed_state, ) except ( - aiohttp.client_exceptions.ClientError, + ClientConnectionError, asyncio.TimeoutError, + AuthenticationError, SensiboError, ) as err: - self._attr_available = False - self.async_write_ha_state() - raise Exception( - f"Failed to set AC state for device {self._attr_name} to Sensibo servers" + raise HomeAssistantError( + f"Failed to set AC state for device {self.name} to Sensibo servers: {err}" ) from err + LOGGER.debug("Result: %s", result) + if result["result"]["status"] == "Success": + self.coordinator.data[self.unique_id][AC_STATE_TO_DATA[name]] = value + self.async_write_ha_state() + return + + failure = result["result"]["failureReason"] + raise HomeAssistantError( + f"Could not set state for device {self.name} due to reason {failure}" + ) + + async def async_assume_state(self, state) -> None: + """Sync state with api.""" + await self._async_set_ac_state_property("on", state != HVAC_MODE_OFF, True) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index 77f1049d8d294..f970581e2a89a 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -6,7 +6,8 @@ import aiohttp import async_timeout -from pysensibo import SensiboClient, SensiboError +from pysensibo import SensiboClient +from pysensibo.exceptions import AuthenticationError, SensiboError import voluptuous as vol from homeassistant import config_entries @@ -16,7 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import _INITIAL_FETCH_FIELDS, DEFAULT_NAME, DOMAIN, TIMEOUT +from .const import DEFAULT_NAME, DOMAIN, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -37,11 +38,12 @@ async def async_validate_api(hass: HomeAssistant, api_key: str) -> bool: try: async with async_timeout.timeout(TIMEOUT): - if await client.async_get_devices(_INITIAL_FETCH_FIELDS): + if await client.async_get_devices(): return True except ( aiohttp.ClientConnectionError, asyncio.TimeoutError, + AuthenticationError, SensiboError, ) as err: _LOGGER.error("Failed to get devices from Sensibo servers %s", err) diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index fb387e64a1a23..683a403cb082e 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,20 +1,25 @@ """Constants for Sensibo.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectionError +from pysensibo.exceptions import AuthenticationError, SensiboError + from homeassistant.const import Platform +LOGGER = logging.getLogger(__package__) + +DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.NUMBER] ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 -_FETCH_FIELDS = ",".join( - [ - "room{name}", - "measurements", - "remoteCapabilities", - "acState", - "connectionStatus{isAlive}", - "temperatureUnit", - ] + +SENSIBO_ERRORS = ( + ClientConnectionError, + asyncio.TimeoutError, + AuthenticationError, + SensiboError, ) -_INITIAL_FETCH_FIELDS = f"id,firmwareVersion,firmwareType,productModel,{_FETCH_FIELDS}" diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py new file mode 100644 index 0000000000000..ef0475640b5af --- /dev/null +++ b/homeassistant/components/sensibo/coordinator.py @@ -0,0 +1,127 @@ +"""DataUpdateCoordinator for the Sensibo integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from pysensibo import SensiboClient +from pysensibo.exceptions import AuthenticationError, SensiboError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT + + +class SensiboDataUpdateCoordinator(DataUpdateCoordinator): + """A Sensibo Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Sensibo coordinator.""" + self.client = SensiboClient( + entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + timeout=TIMEOUT, + ) + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + + async def _async_update_data(self) -> dict[str, dict[str, Any]]: + """Fetch data from Sensibo.""" + + devices = [] + try: + data = await self.client.async_get_devices() + for dev in data["result"]: + devices.append(dev) + except (AuthenticationError, SensiboError) as error: + raise UpdateFailed from error + + device_data: dict[str, dict[str, Any]] = {} + for dev in devices: + unique_id = dev["id"] + mac = dev["macAddress"] + name = dev["room"]["name"] + temperature = dev["measurements"].get("temperature", 0.0) + humidity = dev["measurements"].get("humidity", 0) + ac_states = dev["acState"] + target_temperature = ac_states.get("targetTemperature") + hvac_mode = ac_states.get("mode") + running = ac_states.get("on") + fan_mode = ac_states.get("fanLevel") + swing_mode = ac_states.get("swing") + available = dev["connectionStatus"].get("isAlive", True) + capabilities = dev["remoteCapabilities"] + hvac_modes = list(capabilities["modes"]) + if hvac_modes: + hvac_modes.append("off") + current_capabilities = capabilities["modes"][ac_states.get("mode")] + fan_modes = current_capabilities.get("fanLevels") + swing_modes = current_capabilities.get("swing") + temperature_unit_key = dev.get("temperatureUnit") or ac_states.get( + "temperatureUnit" + ) + temperatures_list = ( + current_capabilities["temperatures"] + .get(temperature_unit_key, {}) + .get("values", [0, 1]) + ) + if temperatures_list: + temperature_step = temperatures_list[1] - temperatures_list[0] + + active_features = list(ac_states) + full_features = set() + for mode in capabilities["modes"]: + if "temperatures" in capabilities["modes"][mode]: + full_features.add("targetTemperature") + if "swing" in capabilities["modes"][mode]: + full_features.add("swing") + if "fanLevels" in capabilities["modes"][mode]: + full_features.add("fanLevel") + + state = hvac_mode if hvac_mode else "off" + + fw_ver = dev["firmwareVersion"] + fw_type = dev["firmwareType"] + model = dev["productModel"] + + calibration_temp = dev["sensorsCalibration"].get("temperature", 0.0) + calibration_hum = dev["sensorsCalibration"].get("humidity", 0.0) + + device_data[unique_id] = { + "id": unique_id, + "mac": mac, + "name": name, + "ac_states": ac_states, + "temp": temperature, + "humidity": humidity, + "target_temp": target_temperature, + "hvac_mode": hvac_mode, + "on": running, + "fan_mode": fan_mode, + "swing_mode": swing_mode, + "available": available, + "hvac_modes": hvac_modes, + "fan_modes": fan_modes, + "swing_modes": swing_modes, + "temp_unit": temperature_unit_key, + "temp_list": temperatures_list, + "temp_step": temperature_step, + "active_features": active_features, + "full_features": full_features, + "state": state, + "fw_ver": fw_ver, + "fw_type": fw_type, + "model": model, + "calibration_temp": calibration_temp, + "calibration_hum": calibration_hum, + "full_capabilities": capabilities, + } + return device_data diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py new file mode 100644 index 0000000000000..d3e2382c7a807 --- /dev/null +++ b/homeassistant/components/sensibo/diagnostics.py @@ -0,0 +1,18 @@ +"""Diagnostics support for Sensibo.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + return coordinator.data diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index bf0142628b4b5..35273bb3d6fdf 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,11 +2,15 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.3"], + "requirements": ["pysensibo==1.0.7"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", "homekit": { "models": ["Sensibo"] - } + }, + "dhcp": [ + {"hostname":"sensibo*"} + ], + "loggers": ["pysensibo"] } diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py new file mode 100644 index 0000000000000..9e531249bf7e7 --- /dev/null +++ b/homeassistant/components/sensibo/number.py @@ -0,0 +1,132 @@ +"""Number platform for Sensibo integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +import async_timeout + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .coordinator import SensiboDataUpdateCoordinator + + +@dataclass +class SensiboEntityDescriptionMixin: + """Mixin values for Sensibo entities.""" + + remote_key: str + + +@dataclass +class SensiboNumberEntityDescription( + NumberEntityDescription, SensiboEntityDescriptionMixin +): + """Class describing Sensibo Number entities.""" + + +NUMBER_TYPES = ( + SensiboNumberEntityDescription( + key="calibration_temp", + remote_key="temperature", + name="Temperature calibration", + icon="mdi:thermometer", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + min_value=-10, + max_value=10, + step=0.1, + ), + SensiboNumberEntityDescription( + key="calibration_hum", + remote_key="humidity", + name="Humidity calibration", + icon="mdi:water", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + min_value=-10, + max_value=10, + step=0.1, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo number platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SensiboNumber(coordinator, device_id, description) + for device_id, device_data in coordinator.data.items() + for description in NUMBER_TYPES + if device_data["hvac_modes"] and device_data["temp"] + ) + + +class SensiboNumber(CoordinatorEntity, NumberEntity): + """Representation of a Sensibo numbers.""" + + coordinator: SensiboDataUpdateCoordinator + entity_description: SensiboNumberEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboNumberEntityDescription, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._device_id = device_id + self._client = coordinator.client + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = ( + f"{coordinator.data[device_id]['name']} {entity_description.name}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, + name=coordinator.data[device_id]["name"], + connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=coordinator.data[device_id]["model"], + sw_version=coordinator.data[device_id]["fw_ver"], + hw_version=coordinator.data[device_id]["fw_type"], + suggested_area=coordinator.data[device_id]["name"], + ) + + @property + def value(self) -> float: + """Return the value from coordinator data.""" + return self.coordinator.data[self._device_id][self.entity_description.key] + + async def async_set_value(self, value: float) -> None: + """Set value for calibration.""" + data = {self.entity_description.remote_key: value} + try: + async with async_timeout.timeout(TIMEOUT): + result = await self._client.async_set_calibration( + self._device_id, + data, + ) + except SENSIBO_ERRORS as err: + raise HomeAssistantError( + f"Failed to set calibration for device {self.name} to Sensibo servers: {err}" + ) from err + LOGGER.debug("Result: %s", result) + if result["status"] == "success": + self.coordinator.data[self._device_id][self.entity_description.key] = value + self.async_write_ha_state() + return + raise HomeAssistantError(f"Could not set calibration for device {self.name}") diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 586ad3b4168f8..bbbdb8611e87b 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -1,18 +1,18 @@ assume_state: name: Assume state description: Set Sensibo device to external state. + target: + entity: + integration: sensibo + domain: climate fields: - entity_id: - name: Entity - description: Name(s) of entities to change. - selector: - entity: - integration: sensibo - domain: climate state: name: State description: State to set. required: true - example: "idle" + example: "on" selector: - text: + select: + options: + - "on" + - "off" diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json new file mode 100644 index 0000000000000..455c89deba4e4 --- /dev/null +++ b/homeassistant/components/sensibo/translations/el.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json new file mode 100644 index 0000000000000..fac1d04755f96 --- /dev/null +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json new file mode 100644 index 0000000000000..694f006218bd9 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2b879c30a1620..3414f13268ff6 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,7 +9,6 @@ import logging from typing import Any, Final, cast, final -import ciso8601 import voluptuous as vol from homeassistant.backports.enum import StrEnum @@ -53,7 +52,9 @@ ) 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, StateType +from homeassistant.util import dt as dt_util from .const import CONF_STATE_CLASS # noqa: F401 @@ -354,7 +355,7 @@ def unit_of_measurement(self) -> str | None: hasattr(self, "_attr_unit_of_measurement") and self._attr_unit_of_measurement is not None ): - return self._attr_unit_of_measurement # type: ignore + return self._attr_unit_of_measurement # type: ignore[unreachable] native_unit_of_measurement = self.native_unit_of_measurement @@ -371,44 +372,6 @@ def state(self) -> Any: value = self.native_value device_class = self.device_class - # We have an old non-datetime value, warn about it and convert it during - # the deprecation period. - if ( - value is not None - and device_class in (DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP) - and not isinstance(value, (date, datetime)) - ): - # Deprecation warning for date/timestamp device classes - if not self.__datetime_as_string_deprecation_logged: - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "%s is providing a string for its state, while the device " - "class is '%s', this is not valid and will be unsupported " - "from Home Assistant 2022.2. Please %s", - self.entity_id, - device_class, - report_issue, - ) - self.__datetime_as_string_deprecation_logged = True - - # Anyways, lets validate the date at least.. - try: - value = ciso8601.parse_datetime(str(value)) - except (ValueError, IndexError) as error: - raise ValueError( - f"Invalid date/datetime: {self.entity_id} provide state '{value}', " - f"while it has device class '{device_class}'" - ) from error - - if value.tzinfo is not None and value.tzinfo != timezone.utc: - value = value.astimezone(timezone.utc) - - # Convert the date object to a standardized state string. - if device_class == DEVICE_CLASS_DATE: - return value.date().isoformat() - - return value.isoformat(timespec="seconds") - # Received a datetime if value is not None and device_class == DEVICE_CLASS_TIMESTAMP: try: @@ -427,17 +390,20 @@ def state(self) -> Any: return value.isoformat(timespec="seconds") except (AttributeError, TypeError) as err: raise ValueError( - f"Invalid datetime: {self.entity_id} has a timestamp device class" + f"Invalid datetime: {self.entity_id} has a timestamp device class " f"but does not provide a datetime state but {type(value)}" ) from err # Received a date value if value is not None and device_class == DEVICE_CLASS_DATE: try: - return value.isoformat() # type: ignore + # We cast the value, to avoid using isinstance, but satisfy + # typechecking. The errors are guarded in this try. + value = cast(date, value) + return value.isoformat() except (AttributeError, TypeError) as err: raise ValueError( - f"Invalid date: {self.entity_id} has a date device class" + f"Invalid date: {self.entity_id} has a date device class " f"but does not provide a date state but {type(value)}" ) from err @@ -471,7 +437,7 @@ def state(self) -> Any: prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 # Suppress ValueError (Could not convert sensor_value to float) with suppress(ValueError): - temp = units.temperature(float(value), unit_of_measurement) # type: ignore + temp = units.temperature(float(value), unit_of_measurement) # type: ignore[arg-type] value = round(temp) if prec == 0 else round(temp, prec) return value @@ -486,3 +452,62 @@ def __repr__(self) -> str: return f"" return super().__repr__() + + +@dataclass +class SensorExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + native_value: StateType | date | datetime + native_unit_of_measurement: str | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the sensor data.""" + native_value: StateType | date | datetime | dict[str, str] = self.native_value + if isinstance(native_value, (date, datetime)): + native_value = { + "__type": str(type(native_value)), + "isoformat": native_value.isoformat(), + } + return { + "native_value": native_value, + "native_unit_of_measurement": self.native_unit_of_measurement, + } + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> SensorExtraStoredData | None: + """Initialize a stored sensor state from a dict.""" + try: + native_value = restored["native_value"] + native_unit_of_measurement = restored["native_unit_of_measurement"] + except KeyError: + return None + try: + type_ = native_value["__type"] + if type_ == "": + native_value = dt_util.parse_datetime(native_value["isoformat"]) + elif type_ == "": + native_value = dt_util.parse_date(native_value["isoformat"]) + except TypeError: + # native_value is not a dict + pass + except KeyError: + # native_value is a dict, but does not have all values + return None + + return cls(native_value, native_unit_of_measurement) + + +class RestoreSensor(SensorEntity, RestoreEntity): + """Mixin class for restoring previous sensor state.""" + + @property + def extra_restore_state_data(self) -> SensorExtraStoredData: + """Return sensor specific state data to be restored.""" + return SensorExtraStoredData(self.native_value, self.native_unit_of_measurement) + + async def async_get_last_sensor_data(self) -> SensorExtraStoredData | None: + """Restore native_value and native_unit_of_measurement.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return SensorExtraStoredData.from_dict(restored_last_extra_data.as_dict()) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 50d09b207a01b..635c5af62422f 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -418,7 +418,7 @@ def _compile_statistics( # noqa: C901 ] history_list = {} if entities_full_history: - history_list = history.get_significant_states_with_session( # type: ignore + history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] hass, session, start - datetime.timedelta.resolution, @@ -432,7 +432,7 @@ def _compile_statistics( # noqa: C901 if "sum" not in wanted_statistics[i.entity_id] ] if entities_significant_history: - _history_list = history.get_significant_states_with_session( # type: ignore + _history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] hass, session, start - datetime.timedelta.resolution, diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index a9ede7713f386..25b4e7bd72b2d 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -1,15 +1,44 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c6\u03b1\u03b9\u03bd\u03bf\u03bc\u03b5\u03bd\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 {entity_name}", + "is_battery_level": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2 {entity_name}", + "is_carbon_dioxide": "\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 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", + "is_carbon_monoxide": "\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 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", + "is_energy": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 {entity_name}", "is_frequency": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", + "is_humidity": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name}", + "is_illuminance": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", + "is_nitrogen_dioxide": "\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 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", + "is_nitrogen_monoxide": "\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 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", + "is_nitrous_oxide": "\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 \u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5 {entity_name}", + "is_ozone": "\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 \u03cc\u03b6\u03bf\u03bd\u03c4\u03bf\u03c2 {entity_name}", + "is_pm1": "\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 PM1 {entity_name}", + "is_pm10": "\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 PM10 {entity_name}", "is_pm25": "\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 {entity_name} PM2.5", + "is_power": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b9\u03c3\u03c7\u03cd\u03c2 {entity_name}", + "is_power_factor": "\u03a4\u03c1\u03ad\u03c7\u03c9\u03bd \u03c3\u03c5\u03bd\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03ae\u03c2 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", + "is_pressure": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c0\u03af\u03b5\u03c3\u03b7 {entity_name}", + "is_reactive_power": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ac\u03b5\u03c1\u03b3\u03b7 \u03b9\u03c3\u03c7\u03cd\u03c2 {entity_name}", + "is_signal_strength": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b9\u03c3\u03c7\u03cd\u03c2 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 {entity_name}", "is_sulphur_dioxide": "\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 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5 {entity_name}", - "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_temperature": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 {entity_name}", + "is_value": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 {entity_name}", + "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}" }, "trigger_type": { + "apparent_power": "\u0395\u03bc\u03c6\u03b1\u03bd\u03b5\u03af\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", + "battery_level": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b5\u03c0\u03b9\u03c0\u03ad\u03b4\u03bf\u03c5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 {entity_name}", + "carbon_dioxide": "\u0397 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "carbon_monoxide": "\u0397 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "current": "{entity_name} \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b5\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2", + "energy": "\u0397 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "frequency": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 {entity_name}", "gas": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5", + "humidity": "\u0397 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "illuminance": "\u039f \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "nitrogen_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", "nitrogen_monoxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", "nitrous_oxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b1\u03b6\u03ce\u03c4\u03bf\u03c5", @@ -17,8 +46,16 @@ "pm1": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM1", "pm10": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM10", "pm25": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM2.5", + "power": "\u0397 \u03b9\u03c3\u03c7\u03cd\u03c2 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "power_factor": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c3\u03c5\u03bd\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03ae \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", + "pressure": "\u0397 \u03c0\u03af\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "reactive_power": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03b1\u03ad\u03c1\u03b3\u03bf\u03c5 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2 {entity_name}", + "signal_strength": "\u0397 \u03b9\u03c3\u03c7\u03cd\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", "sulphur_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5", - "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}" + "temperature": "\u0397 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "value": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 {entity_name} \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9", + "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}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 66aab37914d73..1e13fb79fd2a9 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -34,12 +34,12 @@ "battery_level": "variazioni del livello di batteria di {entity_name} ", "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}", "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", - "current": "variazioni di corrente di {entity_name}", - "energy": "variazioni di energia di {entity_name}", + "current": "Variazioni di corrente di {entity_name}", + "energy": "Variazioni di energia di {entity_name}", "frequency": "{entity_name} cambiamenti di frequenza", "gas": "Variazioni di gas di {entity_name}", - "humidity": "variazioni di umidit\u00e0 di {entity_name} ", - "illuminance": "variazioni dell'illuminazione di {entity_name}", + "humidity": "Variazioni di umidit\u00e0 di {entity_name} ", + "illuminance": "Variazioni dell'illuminazione di {entity_name}", "nitrogen_dioxide": "Variazioni della concentrazione di biossido di azoto di {entity_name}", "nitrogen_monoxide": "Variazioni della concentrazione di monossido di azoto di {entity_name}", "nitrous_oxide": "Variazioni della concentrazione di ossidi di azoto di {entity_name}", @@ -47,14 +47,14 @@ "pm1": "Variazioni della concentrazione di PM1 di {entity_name}", "pm10": "Variazioni della concentrazione di PM10 di {entity_name}", "pm25": "Variazioni della concentrazione di PM2.5 di {entity_name}", - "power": "variazioni di alimentazione di {entity_name}", + "power": "Variazioni di alimentazione di {entity_name}", "power_factor": "variazioni del fattore di potenza di {entity_name}", - "pressure": "variazioni della pressione di {entity_name}", + "pressure": "Variazioni della pressione di {entity_name}", "reactive_power": "variazioni di potenza reattiva di {entity_name}", - "signal_strength": "variazioni della potenza del segnale di {entity_name}", + "signal_strength": "Variazioni della potenza del segnale di {entity_name}", "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", - "temperature": "variazioni di temperatura di {entity_name}", - "value": "{entity_name} valori cambiati", + "temperature": "Variazioni di temperatura di {entity_name}", + "value": "Cambi di valore di {entity_name}", "volatile_organic_compounds": "Variazioni della concentrazione di composti organici volatili di {entity_name}", "voltage": "variazioni di tensione di {entity_name}" } diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index 39f0e35a93b93..05c0a69708c75 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -8,7 +8,7 @@ "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", "is_energy": "obecna energia {entity_name}", "is_frequency": "Obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", - "is_gas": "obecny poziom gazu {entity_name}", + "is_gas": "Obecny poziom gazu {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", "is_nitrogen_dioxide": "obecny poziom st\u0119\u017cenia dwutlenku azotu {entity_name}", diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 337a4d9f3184b..72e1c7ba023be 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -1,10 +1,62 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Pot\u00eancia aparente atual de {entity_name}", + "is_battery_level": "N\u00edvel atual da bateria {entity_name}", + "is_carbon_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de carbono de {entity_name}", + "is_carbon_monoxide": "N\u00edvel de concentra\u00e7\u00e3o de mon\u00f3xido de carbono atual de {entity_name}", + "is_current": "Corrente atual de {entity_name}", + "is_energy": "Energia atual de {entity_name}", + "is_frequency": "Frequ\u00eancia atual de {entity_name}", + "is_gas": "G\u00e1s atual de {entity_name}", "is_humidity": "Humidade atual do(a) {entity_name}", + "is_illuminance": "Luminosidade atual {entity_name}", + "is_nitrogen_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de nitrog\u00eanio de {entity_name}", + "is_nitrogen_monoxide": "N\u00edvel atual de concentra\u00e7\u00e3o de mon\u00f3xido de nitrog\u00eanio de {entity_name}", + "is_nitrous_oxide": "N\u00edvel atual de concentra\u00e7\u00e3o de \u00f3xido nitroso de {entity_name}", + "is_ozone": "N\u00edvel atual de concentra\u00e7\u00e3o de oz\u00f4nio de {entity_name}", + "is_pm1": "N\u00edvel de concentra\u00e7\u00e3o PM1 atual de {entity_name}", + "is_pm10": "N\u00edvel de concentra\u00e7\u00e3o PM10 atual de {entity_name}", + "is_pm25": "N\u00edvel de concentra\u00e7\u00e3o PM2.5 atual de {entity_name}", + "is_power": "Pot\u00eancia atual {entity_name}", + "is_power_factor": "Fator de pot\u00eancia atual de {entity_name}", "is_pressure": "Press\u00e3o atual do(a) {entity_name}", + "is_reactive_power": "Pot\u00eancia reativa atual de {entity_name}", "is_signal_strength": "For\u00e7a do sinal atual do(a) {entity_name}", - "is_temperature": "Temperatura atual do(a) {entity_name}" + "is_sulphur_dioxide": "N\u00edvel atual de concentra\u00e7\u00e3o de di\u00f3xido de enxofre de {entity_name}", + "is_temperature": "Temperatura atual do(a) {entity_name}", + "is_value": "Valor atual de {entity_name}", + "is_volatile_organic_compounds": "N\u00edvel atual de concentra\u00e7\u00e3o de compostos org\u00e2nicos vol\u00e1teis de {entity_name}", + "is_voltage": "Tens\u00e3o atual de {entity_name}" + }, + "trigger_type": { + "apparent_power": "Mudan\u00e7as de poder aparentes de {entity_name}", + "battery_level": "{entity_name} mudan\u00e7as no n\u00edvel da bateria", + "carbon_dioxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de di\u00f3xido de carbono de {entity_name}", + "carbon_monoxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de mon\u00f3xido de carbono de {entity_name}", + "current": "Mudan\u00e7a na corrente de {entity_name}", + "energy": "Mudan\u00e7as na energia de {entity_name}", + "frequency": "Altera\u00e7\u00f5es de frequ\u00eancia de {entity_name}", + "gas": "Mudan\u00e7as de g\u00e1s de {entity_name}", + "humidity": "{entity_name} mudan\u00e7as de umidade", + "illuminance": "{entity_name} mudan\u00e7as de luminosidade", + "nitrogen_dioxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de di\u00f3xido de nitrog\u00eanio de {entity_name}", + "nitrogen_monoxide": "Mudan\u00e7as na concentra\u00e7\u00e3o de mon\u00f3xido de nitrog\u00eanio de {entity_name}", + "nitrous_oxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de \u00f3xido nitroso de {entity_name}", + "ozone": "Mudan\u00e7as na concentra\u00e7\u00e3o de oz\u00f4nio de {entity_name}", + "pm1": "Mudan\u00e7as na concentra\u00e7\u00e3o PM1 de {entity_name}", + "pm10": "Mudan\u00e7as na concentra\u00e7\u00e3o PM10 de {entity_name}", + "pm25": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o PM2.5 de {entity_name}", + "power": "{entity_name} mudan\u00e7as de energia", + "power_factor": "Altera\u00e7\u00f5es do fator de pot\u00eancia de {entity_name}", + "pressure": "{entity_name} mudan\u00e7as de press\u00e3o", + "reactive_power": "Altera\u00e7\u00f5es de pot\u00eancia reativa de {entity_name}", + "signal_strength": "{entity_name} muda a for\u00e7a do sinal", + "sulphur_dioxide": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de di\u00f3xido de enxofre de {entity_name}", + "temperature": "{entity_name} mudan\u00e7as de temperatura", + "value": "{entity_name} mudan\u00e7as de valor", + "volatile_organic_compounds": "Altera\u00e7\u00f5es na concentra\u00e7\u00e3o de compostos org\u00e2nicos vol\u00e1teis de {entity_name}", + "voltage": "Mudan\u00e7as de voltagem de {entity_name}" } }, "state": { diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 9c90067efef4c..52f7cba7a1923 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.5.3"], + "requirements": ["sentry-sdk==1.5.5"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/sentry/translations/el.json b/homeassistant/components/sentry/translations/el.json index 4e39e2bdf6f59..fd64b2140e261 100644 --- a/homeassistant/components/sentry/translations/el.json +++ b/homeassistant/components/sentry/translations/el.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "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": { - "bad_dsn": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf DSN" + "bad_dsn": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf DSN", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { diff --git a/homeassistant/components/sentry/translations/pt-BR.json b/homeassistant/components/sentry/translations/pt-BR.json index b4f025eaf6d63..21f1e3ef91a98 100644 --- a/homeassistant/components/sentry/translations/pt-BR.json +++ b/homeassistant/components/sentry/translations/pt-BR.json @@ -1,12 +1,35 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "error": { "bad_dsn": "DSN inv\u00e1lido", "unknown": "Erro inesperado" }, "step": { "user": { - "description": "Digite seu DSN Sentry" + "data": { + "dsn": "DSN" + }, + "description": "Digite seu DSN Sentry", + "title": "Sentry" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nome opcional do ambiente.", + "event_custom_components": "Enviar eventos de componentes personalizados", + "event_handled": "Enviar eventos tratados", + "event_third_party_packages": "Envie eventos de pacotes de terceiros", + "logging_event_level": "O n\u00edvel de registro Sentry registrar\u00e1 um evento para", + "logging_level": "O n\u00edvel de log Sentry gravar\u00e1 logs como breadcrums para", + "tracing": "Habilitar o rastreamento de desempenho", + "tracing_sample_rate": "Taxa de amostragem de rastreamento; entre 0,0 e 1,0 (1,0 = 100%)" + } } } } diff --git a/homeassistant/components/sentry/translations/uk.json b/homeassistant/components/sentry/translations/uk.json index 01da0308851d3..124ac4543ed89 100644 --- a/homeassistant/components/sentry/translations/uk.json +++ b/homeassistant/components/sentry/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "bad_dsn": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 DSN.", diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index aae10144a661f..04fe4682a4245 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "bad_dsn": "DSN \u7121\u6548", diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index 3812a5de072d7..c427a54779025 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/serial_pm", "requirements": ["pmsensor==0.4"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pmsensor"] } diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index c4a3e3775ae95..c6c4db1143be9 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sesame", "requirements": ["pysesame2==1.0.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pysesame2"] } diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 01fdb22395cdf..227f19d248113 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": ["py17track==2021.12.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["py17track"] } diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index 3299e05222790..0875609db1e94 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/sharkiq", "requirements": ["sharkiqpy==0.1.8"], "codeowners": ["@ajmarks"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["sharkiqpy"] } diff --git a/homeassistant/components/sharkiq/translations/el.json b/homeassistant/components/sharkiq/translations/el.json index 4c6777955253d..83187c3809074 100644 --- a/homeassistant/components/sharkiq/translations/el.json +++ b/homeassistant/components/sharkiq/translations/el.json @@ -1,12 +1,27 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\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": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\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", + "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": { "reauth": { "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\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" + } + }, + "user": { + "data": { + "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/sharkiq/translations/pt-BR.json b/homeassistant/components/sharkiq/translations/pt-BR.json new file mode 100644 index 0000000000000..f16b424dee5c8 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falha ao conectar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/sk.json b/homeassistant/components/sharkiq/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3ab87ad9d0e85..b29079affcf03 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -80,6 +80,7 @@ BLOCK_SLEEPING_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.NUMBER, Platform.SENSOR, ] RPC_PLATFORMS: Final = [ @@ -680,19 +681,17 @@ def _async_device_updates_handler(self) -> None: ENTRY_RELOAD_COOLDOWN, ) self.hass.async_create_task(self._debounced_reload.async_call()) - elif event_type not in RPC_INPUTS_EVENTS_TYPES: - continue - - self.hass.bus.async_fire( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: self.device_id, - ATTR_DEVICE: self.device.hostname, - ATTR_CHANNEL: event["id"] + 1, - ATTR_CLICK_TYPE: event["event"], - ATTR_GENERATION: 2, - }, - ) + elif event_type in RPC_INPUTS_EVENTS_TYPES: + self.hass.bus.async_fire( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE: self.device.hostname, + ATTR_CHANNEL: event["id"] + 1, + ATTR_CLICK_TYPE: event["event"], + ATTR_GENERATION: 2, + }, + ) async def _async_update_data(self) -> None: """Fetch data.""" diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 80ac8133415be..a6cde0c4670d0 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -188,6 +188,33 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr }, entity_category=EntityCategory.DIAGNOSTIC, ), + "overtemp": RpcBinarySensorDescription( + key="switch", + sub_key="errors", + name="Overheating", + device_class=BinarySensorDeviceClass.PROBLEM, + value=lambda status, _: False if status is None else "overtemp" in status, + entity_category=EntityCategory.DIAGNOSTIC, + supported=lambda status: status.get("apower") is not None, + ), + "overpower": RpcBinarySensorDescription( + key="switch", + sub_key="errors", + name="Overpowering", + device_class=BinarySensorDeviceClass.PROBLEM, + value=lambda status, _: False if status is None else "overpower" in status, + entity_category=EntityCategory.DIAGNOSTIC, + supported=lambda status: status.get("apower") is not None, + ), + "overvoltage": RpcBinarySensorDescription( + key="switch", + sub_key="errors", + name="Overvoltage", + device_class=BinarySensorDeviceClass.PROBLEM, + value=lambda status, _: False if status is None else "overvoltage" in status, + entity_category=EntityCategory.DIAGNOSTIC, + supported=lambda status: status.get("apower") is not None, + ), } diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 9a4eb342f71d5..2c81ecbe183ed 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -2,8 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging -from types import MappingProxyType from typing import Any, Final, cast from aioshelly.block_device import Block @@ -140,7 +140,7 @@ def __init__( self.control_result: dict[str, Any] | None = None self.device_block: Block | None = device_block self.last_state: State | None = None - self.last_state_attributes: MappingProxyType[str, Any] + self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] if self.block is not None and self.device_block is not None: diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 12f82016a1c08..51e0711b035b5 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -183,7 +183,9 @@ async def async_setup_entry_rpc( for key in key_instances: # Filter non-existing sensors - if description.sub_key not in wrapper.device.status[key]: + if description.sub_key not in wrapper.device.status[ + key + ] and not description.supported(wrapper.device.status[key]): continue # Filter and remove entities that according to settings should not create an entity @@ -266,6 +268,7 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): removal_condition: Callable[[dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None use_polling_wrapper: bool = False + supported: Callable = lambda _: False @dataclass @@ -505,7 +508,9 @@ def attribute_value(self) -> StateType: """Value of sensor.""" if callable(self.entity_description.value): self._last_value = self.entity_description.value( - self.wrapper.device.status[self.key][self.entity_description.sub_key], + self.wrapper.device.status[self.key].get( + self.entity_description.sub_key + ), self._last_value, ) else: diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1d26970565278..1d4d47748be86 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.9"], + "requirements": ["aioshelly==1.0.11"], "zeroconf": [ { "type": "_http._tcp.local.", @@ -11,5 +11,6 @@ } ], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aioshelly"] } diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py new file mode 100644 index 0000000000000..27773c629c046 --- /dev/null +++ b/homeassistant/components/shelly/number.py @@ -0,0 +1,132 @@ +"""Number for Shelly.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +import logging +from typing import Any, Final, cast + +import async_timeout + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +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.entity_registry import RegistryEntry + +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD +from .entity import ( + BlockEntityDescription, + ShellySleepingBlockAttributeEntity, + async_setup_entry_attribute_entities, +) +from .utils import get_device_entry_gen + +_LOGGER: Final = logging.getLogger(__name__) + + +@dataclass +class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription): + """Class to describe a BLOCK sensor.""" + + mode: NumberMode = NumberMode("slider") + rest_path: str = "" + rest_arg: str = "" + + +NUMBERS: Final = { + ("device", "valvePos"): BlockNumberDescription( + key="device|valvepos", + icon="mdi:pipe-valve", + name="Valve Position", + unit_of_measurement=PERCENTAGE, + available=lambda block: cast(int, block.valveError) != 1, + entity_category=EntityCategory.CONFIG, + min_value=0, + max_value=100, + step=1, + mode=NumberMode("slider"), + rest_path="thermostat/0", + rest_arg="pos", + ), +} + + +def _build_block_description(entry: RegistryEntry) -> BlockNumberDescription: + """Build description when restoring block attribute entities.""" + assert entry.capabilities + return BlockNumberDescription( + key="", + name="", + icon=entry.original_icon, + unit_of_measurement=entry.unit_of_measurement, + device_class=entry.original_device_class, + min_value=cast(float, entry.capabilities.get("min")), + max_value=cast(float, entry.capabilities.get("max")), + step=cast(float, entry.capabilities.get("step")), + mode=cast(NumberMode, entry.capabilities.get("mode")), + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + 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]: + await async_setup_entry_attribute_entities( + hass, + config_entry, + async_add_entities, + NUMBERS, + BlockSleepingNumber, + _build_block_description, + ) + + +class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): + """Represent a block sleeping number.""" + + entity_description: BlockNumberDescription + + @property + def value(self) -> float: + """Return value of number.""" + if self.block is not None: + return cast(float, self.attribute_value) + + return cast(float, self.last_state) + + async def async_set_value(self, value: float) -> None: + """Set value.""" + # Example for Shelly Valve: http://192.168.188.187/thermostat/0?pos=13.0 + await self._set_state_full_path( + self.entity_description.rest_path, + {self.entity_description.rest_arg: value}, + ) + self.async_write_ha_state() + + async def _set_state_full_path(self, path: str, params: Any) -> Any: + """Set block state (HTTP request).""" + + _LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) + try: + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): + return await self.wrapper.device.http_request("get", path, params) + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.error( + "Setting state for entity %s failed, state: %s, error: %s", + self.name, + params, + repr(err), + ) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ce9c57f588966..21a7447e2b285 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -215,6 +215,15 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, ), + ("sensor", "temp"): BlockSensorDescription( + key="sensor|temp", + name="Temperature", + unit_fn=temperature_unit, + value=lambda value: round(value, 1), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index e3f1215d6f2e2..d7b817eb99425 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Chcete nastavit {model} na {host}?\n\nP\u0159ed nastaven\u00edm mus\u00ed b\u00fdt za\u0159\u00edzen\u00ed nap\u00e1jen\u00e9 z baterie probuzeno stisknut\u00edm tla\u010d\u00edtka na dan\u00e9m za\u0159\u00edzen\u00ed." + "description": "Chcete nastavit {model} na adrese {host}? \n\nBateriemi nap\u00e1jen\u00e1 za\u0159\u00edzen\u00ed, kter\u00e1 jsou chr\u00e1n\u011bna heslem, je nutn\u00e9 p\u0159ed pokra\u010dov\u00e1n\u00edm probudit.\nBateriemi nap\u00e1jen\u00e1 za\u0159\u00edzen\u00ed, kter\u00e1 nejsou chr\u00e1n\u011bna heslem, budou p\u0159id\u00e1na po probuzen\u00ed za\u0159\u00edzen\u00ed. Nyn\u00ed m\u016f\u017eete za\u0159\u00edzen\u00ed probudit ru\u010dn\u011b pomoc\u00ed tla\u010d\u00edtka na n\u011bm nebo po\u010dkat na dal\u0161\u00ed aktualizaci dat ze za\u0159\u00edzen\u00ed." }, "credentials": { "data": { @@ -37,11 +37,16 @@ "button4": "\u010ctvrt\u00e9 tla\u010d\u00edtko" }, "trigger_type": { + "btn_down": "\"{subtype}\" stisknuto dol\u016f", + "btn_up": "\"{subtype}\" stisknuto nahoru", "double": "\"{subtype}\" stisknuto dvakr\u00e1t", + "double_push": "\"{subtype}\" stisknuto dvakr\u00e1t", "long": "\"{subtype}\" stisknuto dlouze", + "long_push": "\"{subtype}\" stisknuto dlouze", "long_single": "\"{subtype}\" stisknuto dlouze a pak jednou", "single": "\"{subtype}\" stisknuto jednou", "single_long": "\"{subtype}\" stisknuto jednou a pak dlouze", + "single_push": "\"{subtype}\" stisknuto jednou", "triple": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" } } diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index 1d727ded5d9f7..c4a5289140617 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -1,14 +1,29 @@ { "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", "unsupported_firmware": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \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 \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd." }, + "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" + }, "flow_title": "Shelly: {\u03cc\u03bd\u03bf\u03bc\u03b1}", "step": { "confirm_discovery": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {host};\n\n\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, + "credentials": { + "data": { + "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" + } + }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." } } @@ -18,7 +33,21 @@ "button": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af", "button1": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button2": "\u0394\u03b5\u03cd\u03c4\u03b5\u03c1\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", - "button3": "\u03a4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af" + "button3": "\u03a4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "button4": "\u03a4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af" + }, + "trigger_type": { + "btn_down": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af {subtype} \u03ba\u03ac\u03c4\u03c9", + "btn_up": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af {subtype} \u03b5\u03c0\u03ac\u03bd\u03c9", + "double": "\u0394\u03b9\u03c0\u03bb\u03cc \u03ba\u03bb\u03b9\u03ba \u03c4\u03bf\u03c5 {subtype}", + "double_push": "{subtype} \u03b4\u03b9\u03c0\u03bb\u03ae \u03ce\u03b8\u03b7\u03c3\u03b7", + "long": "\u03a0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b9\u03ba \u03c4\u03bf\u03c5 {subtype}", + "long_push": "{subtype} \u03bc\u03b1\u03ba\u03c1\u03ac \u03ce\u03b8\u03b7\u03c3\u03b7", + "long_single": "\u03a0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b9\u03ba \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03ad\u03bd\u03b1 \u03bc\u03cc\u03bd\u03bf \u03ba\u03bb\u03b9\u03ba \u03c4\u03bf\u03c5 {subtype}", + "single": "\u039c\u03bf\u03bd\u03cc \u03ba\u03bb\u03b9\u03ba \u03c4\u03bf\u03c5 {subtype}", + "single_long": "\u039c\u03bf\u03bd\u03cc \u03ba\u03bb\u03b9\u03ba \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b9\u03ba \u03c4\u03bf\u03c5 {subtype}", + "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}" } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/fa.json b/homeassistant/components/shelly/translations/fa.json new file mode 100644 index 0000000000000..e2a8e761bfc2e --- /dev/null +++ b/homeassistant/components/shelly/translations/fa.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0641\u0627\u0631\u0633\u06cc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index ec20d3a7b26e4..c004141cac47d 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -41,7 +41,7 @@ "btn_up": "{subtype} pulsante in su", "double": "{subtype} premuto due volte", "double_push": "{subtype} doppia pressione", - "long": "{subtype} cliccato a lungo", + "long": "{subtype} premuto a lungo", "long_push": "{subtype} pressione prolungata", "long_single": "{subtype} premuto a lungo e poi singolarmente", "single": "{subtype} premuto singolarmente", diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 062cc73de37f8..d1c658d781687 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -37,16 +37,16 @@ "button4": "Czwarty przycisk" }, "trigger_type": { - "btn_down": "zostanie wci\u015bni\u0119ty przycisk \"w d\u00f3\u0142\" {subtype}", - "btn_up": "zostanie wci\u015bni\u0119ty przycisk \"do g\u00f3ry\" {subtype}", + "btn_down": "przycisk {subtype} zostanie wci\u015bni\u0119ty", + "btn_up": "przycisk {subtype} zostanie puszczony", "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", - "double_push": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", - "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty", + "double_push": "przycisk {subtype} zostanie dwukrotnie naci\u015bni\u0119ty", + "long": "przycisk {subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", "long_push": "przycisk {subtype} zostanie d\u0142ugo naci\u015bni\u0119ty", "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", - "single_push": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", + "single_push": "przycisk {subtype} zostanie pojedynczo naci\u015bni\u0119ty", "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } diff --git a/homeassistant/components/shelly/translations/pt-BR.json b/homeassistant/components/shelly/translations/pt-BR.json new file mode 100644 index 0000000000000..8b8ec5ea020c9 --- /dev/null +++ b/homeassistant/components/shelly/translations/pt-BR.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "unsupported_firmware": "O dispositivo est\u00e1 usando uma vers\u00e3o de firmware n\u00e3o compat\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Deseja configurar o {model} em {host} ? \n\n Os dispositivos alimentados por bateria que s\u00e3o protegidos por senha devem ser ativados antes de continuar com a configura\u00e7\u00e3o.\n Dispositivos alimentados por bateria que n\u00e3o s\u00e3o protegidos por senha ser\u00e3o adicionados quando o dispositivo for ativado, agora voc\u00ea pode ativar manualmente o dispositivo usando um bot\u00e3o ou aguardar a pr\u00f3xima atualiza\u00e7\u00e3o de dados do dispositivo." + }, + "credentials": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Antes de configurar, os dispositivos alimentados por bateria devem ser ativados, agora voc\u00ea pode ativar o dispositivo usando um bot\u00e3o nele." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00e3o", + "button1": "Primeiro bot\u00e3o", + "button2": "Segundo bot\u00e3o", + "button3": "Terceiro bot\u00e3o", + "button4": "Quarto bot\u00e3o" + }, + "trigger_type": { + "btn_down": "{subtype} bot\u00e3o para baixo", + "btn_up": "{subtype} bot\u00e3o para cima", + "double": "{subtype} clicado duas vezes", + "double_push": "{subtype} empurr\u00e3o duplo", + "long": "{subtype} clicado longo", + "long_push": "{subtype} empurr\u00e3o longo", + "long_single": "{subtype} clicado longo e, em seguida, \u00fanico clicado", + "single": "{subtype} \u00fanico clicado", + "single_long": "{subtype} \u00fanico clicado e, em seguida, clique longo", + "single_push": "{subtype} \u00fanico empurr\u00e3o", + "triple": "{subtype} triplo clicado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sk.json b/homeassistant/components/shelly/translations/sk.json new file mode 100644 index 0000000000000..a019d22d2641e --- /dev/null +++ b/homeassistant/components/shelly/translations/sk.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "unsupported_firmware": "Zariadenie pou\u017e\u00edva nepodporovan\u00fa verziu firmv\u00e9ru." + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Chcete nastavi\u0165 {model} na {host}? \n\nZariadenia nap\u00e1jan\u00e9 z bat\u00e9rie, ktor\u00e9 s\u00fa chr\u00e1nen\u00e9 heslom, sa musia prebudi\u0165 ne\u017e budete pokra\u010dova\u0165.\nZariadenia nap\u00e1jan\u00e9 z bat\u00e9rie, ktor\u00e9 nie s\u00fa chr\u00e1nen\u00e9 heslom, sa pridaj\u00fa po prebuden\u00ed zariadenia. Teraz m\u00f4\u017eete zariadenie zobudi\u0165 pomocou tla\u010didla na \u0148om alebo po\u010dka\u0165 na \u010fal\u0161iu aktualiz\u00e1ciu \u00fadajov zo zariadenia." + }, + "credentials": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "description": "Pred nastaven\u00edm musia by\u0165 zariadenia nap\u00e1jan\u00e9 z bat\u00e9rie zobuden\u00e9. Zobu\u010fte zariadenie pomocou tla\u010didla na \u0148om." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "Tla\u010didlo", + "button1": "Prv\u00e9 tla\u010didlo", + "button2": "Druh\u00e9 tla\u010didlo", + "button3": "Tretie tla\u010didlo", + "button4": "\u0160tvrt\u00e9 tla\u010didlo" + }, + "trigger_type": { + "btn_down": "{subtype} stla\u010den\u00e9 dole", + "btn_up": "{subtype} stla\u010den\u00e9 hore", + "double": "{subtype} stla\u010den\u00e9 dvakr\u00e1t", + "double_push": "{subtype} stla\u010den\u00e9 dvakr\u00e1t", + "long": "{subtype} stla\u010den\u00e9 dlho", + "long_push": "{subtype} stla\u010den\u00e9 dlho", + "long_single": "{subtype} stla\u010den\u00e9 dlho a potom raz", + "single": "{subtype} stla\u010den\u00e9 raz", + "single_long": "{subtype} stla\u010den\u00e9 raz a potom dlho", + "single_push": "{subtype} stla\u010den\u00e9 raz", + "triple": "{subtype} stla\u010den\u00e9 trikr\u00e1t" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index fc475c2f48efa..e3d27b6b4fc1c 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": ["paho-mqtt==1.6.1"], "codeowners": ["@fabaff"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["paho"] } diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index bf4aed39cc6a0..49e6a14b71509 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": ["shodan==1.26.1"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["shodan"] } diff --git a/homeassistant/components/shopping_list/translations/el.json b/homeassistant/components/shopping_list/translations/el.json index f5b11bd9d4d24..fe1c8f0f259a4 100644 --- a/homeassistant/components/shopping_list/translations/el.json +++ b/homeassistant/components/shopping_list/translations/el.json @@ -1,9 +1,14 @@ { "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" + }, "step": { "user": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03b3\u03bf\u03c1\u03ce\u03bd;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03b3\u03bf\u03c1\u03ce\u03bd;", + "title": "\u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03b3\u03bf\u03c1\u03ce\u03bd" } } - } + }, + "title": "\u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03b3\u03bf\u03c1\u03ce\u03bd" } \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/pt-BR.json b/homeassistant/components/shopping_list/translations/pt-BR.json index 9e8b24efa29c7..bdb2d4041ef32 100644 --- a/homeassistant/components/shopping_list/translations/pt-BR.json +++ b/homeassistant/components/shopping_list/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A lista de compras j\u00e1 est\u00e1 configurada." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 4743c5f040144..0a2a17db2005d 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -12,7 +12,6 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_PORT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -24,16 +23,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import ( - CONF_ACCOUNT, - CONF_ACCOUNTS, - CONF_PING_INTERVAL, - CONF_ZONES, - KEY_ALARM, - PREVIOUS_STATE, - SIA_NAME_FORMAT, - SIA_UNIQUE_ID_FORMAT_ALARM, -) +from .const import CONF_ACCOUNT, CONF_ACCOUNTS, CONF_ZONES, KEY_ALARM, PREVIOUS_STATE from .sia_entity_base import SIABaseEntity, SIAEntityDescription _LOGGER = logging.getLogger(__name__) @@ -86,17 +76,7 @@ async def async_setup_entry( """Set up SIA alarm_control_panel(s) from a config entry.""" async_add_entities( SIAAlarmControlPanel( - port=entry.data[CONF_PORT], - account=account_data[CONF_ACCOUNT], - zone=zone, - ping_interval=account_data[CONF_PING_INTERVAL], - entity_description=ENTITY_DESCRIPTION_ALARM, - unique_id=SIA_UNIQUE_ID_FORMAT_ALARM.format( - entry.entry_id, account_data[CONF_ACCOUNT], zone - ), - name=SIA_NAME_FORMAT.format( - entry.data[CONF_PORT], account_data[CONF_ACCOUNT], zone, "alarm" - ), + entry, account_data[CONF_ACCOUNT], zone, ENTITY_DESCRIPTION_ALARM ) for account_data in entry.data[CONF_ACCOUNTS] for zone in range( @@ -114,23 +94,17 @@ class SIAAlarmControlPanel(SIABaseEntity, AlarmControlPanelEntity): def __init__( self, - port: int, + entry: ConfigEntry, account: str, - zone: int | None, - ping_interval: int, + zone: int, entity_description: SIAAlarmControlPanelEntityDescription, - unique_id: str, - name: str, ) -> None: """Create SIAAlarmControlPanel object.""" super().__init__( - port, + entry, account, zone, - ping_interval, entity_description, - unique_id, - name, ) self._attr_state: StateType = None @@ -144,7 +118,10 @@ def handle_last_state(self, last_state: State | None) -> None: self._attr_available = False def update_state(self, sia_event: SIAEvent) -> bool: - """Update the state of the alarm control panel.""" + """Update the state of the alarm control panel. + + Return True if the event was relevant for this entity. + """ new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index e26e26cc0b7e7..f23bd2885a67d 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -13,23 +13,20 @@ BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant, State +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_ACCOUNT, CONF_ACCOUNTS, - CONF_PING_INTERVAL, CONF_ZONES, + KEY_CONNECTIVITY, KEY_MOISTURE, KEY_POWER, KEY_SMOKE, SIA_HUB_ZONE, - SIA_NAME_FORMAT, - SIA_NAME_FORMAT_HUB, - SIA_UNIQUE_ID_FORMAT_BINARY, ) from .sia_entity_base import SIABaseEntity, SIAEntityDescription @@ -78,72 +75,31 @@ class SIABinarySensorEntityDescription( entity_registry_enabled_default=False, ) +ENTITY_DESCRIPTION_CONNECTIVITY = SIABinarySensorEntityDescription( + key=KEY_CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + code_consequences={"RP": True}, +) -def generate_binary_sensors(entry) -> Iterable[SIABinarySensor]: + +def generate_binary_sensors(entry: ConfigEntry) -> Iterable[SIABinarySensor]: """Generate binary sensors. For each Account there is one power sensor with zone == 0. For each Zone in each Account there is one smoke and one moisture sensor. """ for account_data in entry.data[CONF_ACCOUNTS]: - yield SIABinarySensor( - port=entry.data[CONF_PORT], - account=account_data[CONF_ACCOUNT], - zone=SIA_HUB_ZONE, - ping_interval=account_data[CONF_PING_INTERVAL], - entity_description=ENTITY_DESCRIPTION_POWER, - unique_id=SIA_UNIQUE_ID_FORMAT_BINARY.format( - entry.entry_id, - account_data[CONF_ACCOUNT], - SIA_HUB_ZONE, - ENTITY_DESCRIPTION_POWER.device_class, - ), - name=SIA_NAME_FORMAT_HUB.format( - entry.data[CONF_PORT], - account_data[CONF_ACCOUNT], - ENTITY_DESCRIPTION_POWER.device_class, - ), + account = account_data[CONF_ACCOUNT] + zones = entry.options[CONF_ACCOUNTS][account][CONF_ZONES] + + yield SIABinarySensorConnectivity( + entry, account, SIA_HUB_ZONE, ENTITY_DESCRIPTION_CONNECTIVITY ) - zones = entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + yield SIABinarySensor(entry, account, SIA_HUB_ZONE, ENTITY_DESCRIPTION_POWER) for zone in range(1, zones + 1): - yield SIABinarySensor( - port=entry.data[CONF_PORT], - account=account_data[CONF_ACCOUNT], - zone=zone, - ping_interval=account_data[CONF_PING_INTERVAL], - entity_description=ENTITY_DESCRIPTION_SMOKE, - unique_id=SIA_UNIQUE_ID_FORMAT_BINARY.format( - entry.entry_id, - account_data[CONF_ACCOUNT], - zone, - ENTITY_DESCRIPTION_SMOKE.device_class, - ), - name=SIA_NAME_FORMAT.format( - entry.data[CONF_PORT], - account_data[CONF_ACCOUNT], - zone, - ENTITY_DESCRIPTION_SMOKE.device_class, - ), - ) - yield SIABinarySensor( - port=entry.data[CONF_PORT], - account=account_data[CONF_ACCOUNT], - zone=zone, - ping_interval=account_data[CONF_PING_INTERVAL], - entity_description=ENTITY_DESCRIPTION_MOISTURE, - unique_id=SIA_UNIQUE_ID_FORMAT_BINARY.format( - entry.entry_id, - account_data[CONF_ACCOUNT], - zone, - ENTITY_DESCRIPTION_MOISTURE.device_class, - ), - name=SIA_NAME_FORMAT.format( - entry.data[CONF_PORT], - account_data[CONF_ACCOUNT], - zone, - ENTITY_DESCRIPTION_MOISTURE.device_class, - ), - ) + yield SIABinarySensor(entry, account, zone, ENTITY_DESCRIPTION_SMOKE) + yield SIABinarySensor(entry, account, zone, ENTITY_DESCRIPTION_MOISTURE) async def async_setup_entry( @@ -171,10 +127,23 @@ def handle_last_state(self, last_state: State | None) -> None: self._attr_available = False def update_state(self, sia_event: SIAEvent) -> bool: - """Update the state of the binary sensor.""" + """Update the state of the binary sensor. + + Return True if the event was relevant for this entity. + """ new_state = self.entity_description.code_consequences.get(sia_event.code) if new_state is None: return False _LOGGER.debug("New state will be %s", new_state) self._attr_is_on = bool(new_state) return True + + +class SIABinarySensorConnectivity(SIABinarySensor): + """Class for Connectivity Sensor.""" + + @callback + def async_post_interval_update(self, _) -> None: + """Update state after a ping interval. Overwritten from sia entity base.""" + self._attr_is_on = False + self.async_write_ha_state() diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index 537c106fefa41..82783611e070f 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -24,18 +24,14 @@ CONF_PING_INTERVAL: Final = "ping_interval" CONF_ZONES: Final = "zones" -SIA_NAME_FORMAT: Final = "{} - {} - zone {} - {}" -SIA_NAME_FORMAT_HUB: Final = "{} - {} - {}" -SIA_UNIQUE_ID_FORMAT_ALARM: Final = "{}_{}_{}" -SIA_UNIQUE_ID_FORMAT_BINARY: Final = "{}_{}_{}_{}" -SIA_UNIQUE_ID_FORMAT_HUB: Final = "{}_{}_{}" SIA_HUB_ZONE: Final = 0 SIA_EVENT: Final = "sia_event_{}_{}" -KEY_ALARM: Final = "alarm_control_panel" +KEY_ALARM: Final = "alarm" KEY_SMOKE: Final = "smoke" KEY_MOISTURE: Final = "moisture" KEY_POWER: Final = "power" +KEY_CONNECTIVITY: Final = "connectivity" PREVIOUS_STATE: Final = "previous_state" AVAILABILITY_EVENT_CODE: Final = "RP" diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index c6a8e491217cb..094b04f630658 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/sia", "requirements": ["pysiaalarm==3.0.2"], "codeowners": ["@eavanvalkenburg"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pysiaalarm"] } diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 311728ad578b7..8627dea28bc6c 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -7,6 +7,8 @@ from pysiaalarm import SIAEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityDescription @@ -14,8 +16,20 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType -from .const import AVAILABILITY_EVENT_CODE, DOMAIN, SIA_EVENT, SIA_HUB_ZONE -from .utils import get_attr_from_sia_event, get_unavailability_interval +from .const import ( + AVAILABILITY_EVENT_CODE, + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_PING_INTERVAL, + DOMAIN, + SIA_EVENT, + SIA_HUB_ZONE, +) +from .utils import ( + get_attr_from_sia_event, + get_unavailability_interval, + get_unique_id_and_name, +) _LOGGER = logging.getLogger(__name__) @@ -39,29 +53,32 @@ class SIABaseEntity(RestoreEntity): def __init__( self, - port: int, + entry: ConfigEntry, account: str, - zone: int | None, - ping_interval: int, + zone: int, entity_description: SIAEntityDescription, - unique_id: str, - name: str, ) -> None: """Create SIABaseEntity object.""" - self.port = port + self.port = entry.data[CONF_PORT] self.account = account self.zone = zone - self.ping_interval = ping_interval self.entity_description = entity_description - self._attr_unique_id = unique_id - self._attr_name = name + + self.ping_interval: int = next( + acc[CONF_PING_INTERVAL] + for acc in entry.data[CONF_ACCOUNTS] + if acc[CONF_ACCOUNT] == account + ) + self._attr_unique_id, self._attr_name = get_unique_id_and_name( + entry.entry_id, entry.data[CONF_PORT], account, zone, entity_description.key + ) self._attr_device_info = DeviceInfo( - name=name, - identifiers={(DOMAIN, unique_id)}, - via_device=(DOMAIN, f"{port}_{account}"), + name=self._attr_name, + identifiers={(DOMAIN, self._attr_unique_id)}, + via_device=(DOMAIN, f"{entry.data[CONF_PORT]}_{account}"), ) - self._cancel_availability_cb: CALLBACK_TYPE | None = None + self._post_interval_update_cb_canceller: CALLBACK_TYPE | None = None self._attr_extra_state_attributes = {} self._attr_should_poll = False @@ -83,7 +100,7 @@ async def async_added_to_hass(self) -> None: ) self.handle_last_state(await self.async_get_last_state()) if self._attr_available: - self.async_create_availability_cb() + self.async_create_post_interval_update_cb() @abstractmethod def handle_last_state(self, last_state: State | None) -> None: @@ -94,43 +111,57 @@ async def async_will_remove_from_hass(self) -> None: Overridden from Entity. """ - if self._cancel_availability_cb: - self._cancel_availability_cb() + self._cancel_post_interval_update_cb() @callback def async_handle_event(self, sia_event: SIAEvent) -> None: - """Listen to dispatcher events for this port and account and update state and attributes.""" + """Listen to dispatcher events for this port and account and update state and attributes. + + If the event is for either the zone or the 0 zone (hub zone), then handle it further. + If the event had a code that was relevant for the entity, then update the attributes. + If the event had a code that was relevant or it was a availability event then update the availability and schedule the next unavailability check. + """ _LOGGER.debug("Received event: %s", sia_event) if int(sia_event.ri) not in (self.zone, SIA_HUB_ZONE): return - self._attr_extra_state_attributes.update(get_attr_from_sia_event(sia_event)) - state_changed = self.update_state(sia_event) - if state_changed or sia_event.code == AVAILABILITY_EVENT_CODE: - self.async_reset_availability_cb() + + relevant_event = self.update_state(sia_event) + + if relevant_event: + self._attr_extra_state_attributes.update(get_attr_from_sia_event(sia_event)) + + if relevant_event or sia_event.code == AVAILABILITY_EVENT_CODE: + self._attr_available = True + self._cancel_post_interval_update_cb() + self.async_create_post_interval_update_cb() + self.async_write_ha_state() @abstractmethod def update_state(self, sia_event: SIAEvent) -> bool: - """Do the entity specific state updates.""" + """Do the entity specific state updates. + + Return True if the event was relevant for this entity. + """ @callback - def async_reset_availability_cb(self) -> None: - """Reset availability cb by cancelling the current and creating a new one.""" - self._attr_available = True - if self._cancel_availability_cb: - self._cancel_availability_cb() - self.async_create_availability_cb() - - def async_create_availability_cb(self) -> None: - """Create a availability cb and return the callback.""" - self._cancel_availability_cb = async_call_later( + def async_create_post_interval_update_cb(self) -> None: + """Create a port interval update cb and store the callback.""" + self._post_interval_update_cb_canceller = async_call_later( self.hass, get_unavailability_interval(self.ping_interval), - self.async_set_unavailable, + self.async_post_interval_update, ) @callback - def async_set_unavailable(self, _) -> None: - """Set unavailable.""" + def async_post_interval_update(self, _) -> None: + """Set unavailable after a ping interval.""" self._attr_available = False self.async_write_ha_state() + + @callback + def _cancel_post_interval_update_cb(self) -> None: + """Cancel the callback.""" + if self._post_interval_update_cb_canceller: + self._post_interval_update_cb_canceller() + self._post_interval_update_cb_canceller = None diff --git a/homeassistant/components/sia/translations/cs.json b/homeassistant/components/sia/translations/cs.json new file mode 100644 index 0000000000000..7940c6378feca --- /dev/null +++ b/homeassistant/components/sia/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/el.json b/homeassistant/components/sia/translations/el.json index 10d7ed2757b15..fe565e503a14d 100644 --- a/homeassistant/components/sia/translations/el.json +++ b/homeassistant/components/sia/translations/el.json @@ -1,7 +1,23 @@ { "config": { + "error": { + "invalid_account_format": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c4\u03b9\u03bc\u03ae, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf 0-9 \u03ba\u03b1\u03b9 A-F.", + "invalid_account_length": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf \u03c3\u03c9\u03c3\u03c4\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 3 \u03ba\u03b1\u03b9 16 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "invalid_key_format": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c4\u03b9\u03bc\u03ae, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf 0-9 \u03ba\u03b1\u03b9 A-F.", + "invalid_key_length": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf \u03c3\u03c9\u03c3\u03c4\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 16, 24 \u03ae 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2.", + "invalid_ping": "\u03a4\u03bf \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 ping \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 1 \u03ba\u03b1\u03b9 1440 \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd.", + "invalid_zones": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd 1 \u03b6\u03ce\u03bd\u03b7.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "additional_account": { + "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", + "additional_account": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af", + "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", + "ping_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 ping (\u03bb\u03b5\u03c0\u03c4\u03ac)", + "zones": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03c9\u03bd\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" + }, "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03ac\u03bb\u03bb\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03c4\u03b7\u03bd \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b8\u03cd\u03c1\u03b1." }, "user": { @@ -10,6 +26,7 @@ "additional_account": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af", "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", "ping_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 ping (\u03bb\u03b5\u03c0\u03c4\u03ac)", + "port": "\u0398\u03cd\u03c1\u03b1", "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf", "zones": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03c9\u03bd\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" }, @@ -21,9 +38,13 @@ "step": { "options": { "data": { - "ignore_timestamps": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b7\u03c2 \u03c7\u03c1\u03bf\u03bd\u03bf\u03c3\u03c6\u03c1\u03b1\u03b3\u03af\u03b4\u03b1\u03c2 \u03c4\u03c9\u03bd \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd SIA" - } + "ignore_timestamps": "\u0391\u03b3\u03bd\u03bf\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b7\u03c2 \u03c7\u03c1\u03bf\u03bd\u03bf\u03c3\u03c6\u03c1\u03b1\u03b3\u03af\u03b4\u03b1\u03c2 \u03c4\u03c9\u03bd \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd SIA", + "zones": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03c9\u03bd\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc" + }, + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc: {account}", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SIA." } } - } + }, + "title": "\u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03a3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd SIA" } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/id.json b/homeassistant/components/sia/translations/id.json index e7ab7918fb3e4..4c80d089cbe82 100644 --- a/homeassistant/components/sia/translations/id.json +++ b/homeassistant/components/sia/translations/id.json @@ -4,7 +4,7 @@ "invalid_account_format": "Format akun ini tidak dalam nilai heksadesimal, gunakan hanya karakter 0-9 dan A-F.", "invalid_account_length": "Panjang format akun tidak tepat, harus antara 3 dan 16 karakter.", "invalid_key_format": "Format kunci ini tidak dalam nilai heksadesimal, gunakan hanya karakter 0-9 dan A-F.", - "invalid_key_length": "Panjang format kunci tidak tepat, harus antara 16, 25, atau 32 karakter heksadesimal.", + "invalid_key_length": "Panjang format kunci tidak tepat, harus antara 16, 24, atau 32 karakter heksadesimal.", "invalid_ping": "Interval ping harus antara 1 dan 1440 menit.", "invalid_zones": "Setidaknya harus ada 1 zona.", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/sia/translations/pt-BR.json b/homeassistant/components/sia/translations/pt-BR.json new file mode 100644 index 0000000000000..a113717859fd7 --- /dev/null +++ b/homeassistant/components/sia/translations/pt-BR.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "A conta n\u00e3o \u00e9 um valor hexadecimal, use apenas 0-9 e AF.", + "invalid_account_length": "A conta n\u00e3o tem o tamanho certo, tem que ter entre 3 e 16 caracteres.", + "invalid_key_format": "A chave n\u00e3o \u00e9 um valor hexadecimal, use apenas 0-9 e AF.", + "invalid_key_length": "A chave n\u00e3o tem o tamanho certo, tem que ter 16, 24 ou 32 caracteres hexadecimais.", + "invalid_ping": "O intervalo de ping precisa estar entre 1 e 1440 minutos.", + "invalid_zones": "Deve haver pelo menos 1 zona.", + "unknown": "Erro inesperado" + }, + "step": { + "additional_account": { + "data": { + "account": "ID da conta", + "additional_account": "Contas adicionais", + "encryption_key": "Chave de encripta\u00e7\u00e3o", + "ping_interval": "Intervalo de ping (min)", + "zones": "N\u00famero de zonas para a conta" + }, + "title": "Adicione outra conta \u00e0 porta atual." + }, + "user": { + "data": { + "account": "ID da conta", + "additional_account": "Contas adicionais", + "encryption_key": "Chave de encripta\u00e7\u00e3o", + "ping_interval": "Intervalo de ping (min)", + "port": "Porta", + "protocol": "Protocolo", + "zones": "N\u00famero de zonas para a conta" + }, + "title": "Crie uma conex\u00e3o para sistemas de alarme baseados em SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignore a verifica\u00e7\u00e3o do timestamp dos eventos do SIA", + "zones": "N\u00famero de zonas para a conta" + }, + "description": "Defina as op\u00e7\u00f5es da conta: {account}", + "title": "Op\u00e7\u00f5es para a configura\u00e7\u00e3o SIA." + } + } + }, + "title": "Sistemas de alarme SIA" +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/sk.json b/homeassistant/components/sia/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/sia/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 9150099656c9e..cf52122a4999d 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -8,11 +8,41 @@ from homeassistant.util.dt import utcnow -from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE +from .const import ( + ATTR_CODE, + ATTR_ID, + ATTR_MESSAGE, + ATTR_TIMESTAMP, + ATTR_ZONE, + KEY_ALARM, + SIA_HUB_ZONE, +) PING_INTERVAL_MARGIN = 30 +def get_unique_id_and_name( + entry_id: str, + port: int, + account: str, + zone: int, + entity_key: str, +) -> tuple[str, str]: + """Return the unique_id and name for an entity.""" + return ( + ( + f"{entry_id}_{account}_{zone}" + if entity_key == KEY_ALARM + else f"{entry_id}_{account}_{zone}_{entity_key}" + ), + ( + f"{port} - {account} - {entity_key}" + if zone == SIA_HUB_ZONE + else f"{port} - {account} - zone {zone} - {entity_key}" + ), + ) + + def get_unavailability_interval(ping: int) -> float: """Return the interval to the next unavailability check.""" return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 8edef306d8d2b..92baec1e42b26 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "requirements": ["pillow==9.0.1", "simplehound==0.3"], "codeowners": ["@robmarkcole"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["simplehound"] } diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index 0b5d0febbe771..e95760fc1e0c0 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -8,5 +8,6 @@ "requirements": [ "pysignalclirestapi==0.3.18" ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pysignalclirestapi"] } \ No newline at end of file diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index dc711df0e8d3a..26321d17aefb1 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": ["simplepush==1.1.4"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2fd7e10a2f84b..77732f4e2cb89 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Callable, Iterable from datetime import timedelta -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from simplipy import API from simplipy.device import Device, DeviceTypes @@ -150,86 +150,62 @@ SERVICE_NAME_SET_SYSTEM_PROPERTIES, ) -SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.All( - cv.deprecated(ATTR_SYSTEM_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_SYSTEM_ID): cv.string, - } - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +SERVICE_CLEAR_NOTIFICATIONS_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + }, ) -SERVICE_REMOVE_PIN_SCHEMA = vol.All( - cv.deprecated(ATTR_SYSTEM_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, - } - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + } ) -SERVICE_SET_PIN_SCHEMA = vol.All( - cv.deprecated(ATTR_SYSTEM_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, - }, - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +SERVICE_SET_PIN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_PIN_LABEL): cv.string, + vol.Required(ATTR_PIN_VALUE): cv.string, + }, ) -SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.All( - cv.deprecated(ATTR_SYSTEM_ID), - vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): cv.string, - vol.Optional(ATTR_SYSTEM_ID): cv.string, - vol.Optional(ATTR_ALARM_DURATION): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), - ), - vol.Optional(ATTR_ALARM_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - vol.Optional(ATTR_CHIME_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), - ), - vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_ENTRY_DELAY_HOME), - ), - vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), - ), - vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( - cv.time_period, - lambda value: value.total_seconds(), - vol.Range(max=MAX_EXIT_DELAY_HOME), - ), - vol.Optional(ATTR_LIGHT): cv.boolean, - vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( - vol.In(VOLUME_MAP), VOLUME_MAP.get - ), - } - ), - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_SYSTEM_ID), +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Optional(ATTR_ALARM_DURATION): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ALARM_DURATION, max=MAX_ALARM_DURATION), + ), + vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), + vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.In(VOLUME_MAP), VOLUME_MAP.get), + vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_ENTRY_DELAY_AWAY, max=MAX_ENTRY_DELAY_AWAY), + ), + vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_ENTRY_DELAY_HOME), + ), + vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(min=MIN_EXIT_DELAY_AWAY, max=MAX_EXIT_DELAY_AWAY), + ), + vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( + cv.time_period, + lambda value: value.total_seconds(), + vol.Range(max=MAX_EXIT_DELAY_HOME), + ), + vol.Optional(ATTR_LIGHT): cv.boolean, + vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( + vol.In(VOLUME_MAP), VOLUME_MAP.get + ), + } ) WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] @@ -251,15 +227,6 @@ def _async_get_system_for_service_call( hass: HomeAssistant, call: ServiceCall ) -> SystemType: """Get the SimpliSafe system related to a service call (by device ID).""" - if ATTR_SYSTEM_ID in call.data: - for entry in hass.config_entries.async_entries(DOMAIN): - simplisafe = hass.data[DOMAIN][entry.entry_id] - if ( - system := simplisafe.systems.get(int(call.data[ATTR_SYSTEM_ID])) - ) is None: - continue - return cast(SystemType, system) - device_id = call.data[ATTR_DEVICE_ID] device_registry = dr.async_get(hass) @@ -268,8 +235,7 @@ def _async_get_system_for_service_call( ) is None: raise vol.Invalid("Invalid device ID specified") - if TYPE_CHECKING: - assert alarm_control_panel_device_entry.via_device_id + assert alarm_control_panel_device_entry.via_device_id if ( base_station_device_entry := device_registry.async_get( @@ -527,10 +493,7 @@ def _async_process_new_notifications(self, system: SystemType) -> None: async def _async_start_websocket_loop(self) -> None: """Start a websocket reconnection loop.""" - if TYPE_CHECKING: - assert self._api.websocket - - should_reconnect = True + assert self._api.websocket try: await self._api.websocket.async_connect() @@ -543,12 +506,11 @@ async def _async_start_websocket_loop(self) -> None: except Exception as err: # pylint: disable=broad-except LOGGER.error("Unknown exception while connecting to websocket: %s", err) - if should_reconnect: - LOGGER.info("Disconnected from websocket; reconnecting") - await self._async_cancel_websocket_loop() - self._websocket_reconnect_task = self._hass.async_create_task( - self._async_start_websocket_loop() - ) + LOGGER.info("Reconnecting to websocket") + await self._async_cancel_websocket_loop() + self._websocket_reconnect_task = self._hass.async_create_task( + self._async_start_websocket_loop() + ) async def _async_cancel_websocket_loop(self) -> None: """Stop any existing websocket reconnection loop.""" @@ -560,8 +522,7 @@ async def _async_cancel_websocket_loop(self) -> None: LOGGER.debug("Websocket reconnection task successfully canceled") self._websocket_reconnect_task = None - if TYPE_CHECKING: - assert self._api.websocket + assert self._api.websocket await self._api.websocket.async_disconnect() @callback @@ -598,9 +559,8 @@ def _async_websocket_on_event(self, event: WebsocketEvent) -> None: async def async_init(self) -> None: """Initialize the SimpliSafe "manager" class.""" - if TYPE_CHECKING: - assert self._api.refresh_token - assert self._api.websocket + assert self._api.refresh_token + assert self._api.websocket self._api.websocket.add_event_callback(self._async_websocket_on_event) self._websocket_reconnect_task = asyncio.create_task( @@ -609,9 +569,7 @@ async def async_init(self) -> None: async def async_websocket_disconnect_listener(_: Event) -> None: """Define an event handler to disconnect from the websocket.""" - if TYPE_CHECKING: - assert self._api.websocket - + assert self._api.websocket await self._async_cancel_websocket_loop() self.entry.async_on_unload( @@ -658,10 +616,8 @@ async def async_handle_refresh_token(token: str) -> None: """Handle a new refresh token.""" async_save_refresh_token(token) - if TYPE_CHECKING: - assert self._api.websocket - # Open a new websocket connection with the fresh token: + assert self._api.websocket await self._async_cancel_websocket_loop() self._websocket_reconnect_task = self._hass.async_create_task( self._async_start_websocket_loop() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 43bcaee205923..cf896c3a320f3 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,8 +1,6 @@ """Support for SimpliSafe alarm control panels.""" from __future__ import annotations -from typing import TYPE_CHECKING - from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v3 import SystemV3 @@ -240,8 +238,9 @@ def async_update_from_rest_api(self) -> None: def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: """Update the entity when new data comes from the websocket.""" self._attr_changed_by = event.changed_by - if TYPE_CHECKING: - assert event.event_type + + assert event.event_type + if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type): self._attr_state = state self.async_reset_error_count() diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 3a3d1963e0e4f..ad6e01f0422ee 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the SimpliSafe component.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import Any, NamedTuple from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError @@ -18,7 +18,6 @@ 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.typing import ConfigType from .const import CONF_USER_ID, DOMAIN, LOGGER @@ -85,7 +84,7 @@ def _async_show_form(self, *, errors: dict[str, Any] | None = None) -> FlowResul }, ) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config.get(CONF_USERNAME) self._reauth = True @@ -98,8 +97,7 @@ async def async_step_user( if user_input is None: return self._async_show_form() - if TYPE_CHECKING: - assert self._oauth_values + assert self._oauth_values errors = {} session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 14816cdd579e4..1e7be48979b03 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,7 +1,7 @@ """Support for SimpliSafe locks.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any from simplipy.device.lock import Lock, LockStates from simplipy.errors import SimplipyError @@ -100,8 +100,8 @@ def async_update_from_rest_api(self) -> None: @callback def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: """Update the entity when new data comes from the websocket.""" - if TYPE_CHECKING: - assert event.event_type + assert event.event_type + if state := STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type) is not None: self._attr_is_locked = state self.async_reset_error_count() diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index a5a4b5f48213d..deb9577d57674 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -11,5 +11,6 @@ "hostname": "simplisafe*", "macaddress": "30AEA4*" } - ] + ], + "loggers": ["simplipy"] } diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index cd55de41a547c..d35c59bcc409e 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -1,24 +1,55 @@ { "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." + "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.", + "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." }, "error": { - "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2" + "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\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", + "still_awaiting_mfa": "\u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf email \u03c4\u03bf\u03c5 \u03a5\u03c0\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03bf\u03c5 \u039f\u03b9\u03ba\u03bf\u03bd\u03bf\u03bc\u03b9\u03ba\u03ce\u03bd", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SimpliSafe web:", + "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2" + }, "mfa": { + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf email \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd SimpliSafe. \u0391\u03c6\u03bf\u03cd \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.", "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd SimpliSafe" }, "reauth_confirm": { - "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." + "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" }, "user": { "data": { - "code": "\u039a\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf UI \u03c4\u03bf\u03c5 Home Assistant)" + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", + "code": "\u039a\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf UI \u03c4\u03bf\u03c5 Home Assistant)", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" }, + "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.", "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u039a\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf UI \u03c4\u03bf\u03c5 Home Assistant)" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 0e5b2151e20bb..d1473074ccaed 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -1,20 +1,54 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." + }, "error": { - "identifier_exists": "Conta j\u00e1 cadastrada" + "identifier_exists": "Conta j\u00e1 cadastrada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "still_awaiting_mfa": "Ainda aguardando clique no e-mail da MFA", + "unknown": "Erro inesperado" }, "step": { "input_auth_code": { "data": { "auth_code": "C\u00f3digo de Autoriza\u00e7\u00e3o" - } + }, + "description": "Insira o c\u00f3digo de autoriza\u00e7\u00e3o do URL do aplicativo Web SimpliSafe:", + "title": "Concluir autoriza\u00e7\u00e3o" + }, + "mfa": { + "description": "Verifique seu e-mail para obter um link do SimpliSafe. Ap\u00f3s verificar o link, volte aqui para concluir a instala\u00e7\u00e3o da integra\u00e7\u00e3o.", + "title": "Autentica\u00e7\u00e3o SimpliSafe multifator" + }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Seu acesso expirou ou foi revogado. Digite sua senha para vincular novamente sua conta.", + "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { + "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", + "code": "C\u00f3digo (usado na IU do Home Assistant)", "password": "Senha", - "username": "Endere\u00e7o de e-mail" + "username": "Email" + }, + "description": "O SimpliSafe autentica com o Home Assistant por meio do aplicativo da Web SimpliSafe. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o]( {docs_url} ) antes de come\u00e7ar. \n\n 1. Clique [aqui]( {url} ) para abrir o aplicativo da web SimpliSafe e insira suas credenciais. \n\n 2. Quando o processo de login estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o abaixo.", + "title": "Preencha suas informa\u00e7\u00f5es." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code": "C\u00f3digo (usado na IU do Home Assistant)" }, - "title": "Preencha suas informa\u00e7\u00f5es" + "title": "Configurar SimpliSafe" } } } diff --git a/homeassistant/components/simplisafe/translations/sk.json b/homeassistant/components/simplisafe/translations/sk.json new file mode 100644 index 0000000000000..f59069125d0a1 --- /dev/null +++ b/homeassistant/components/simplisafe/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json index c33babf4913f8..43b9e465f52b2 100644 --- a/homeassistant/components/sinch/manifest.json +++ b/homeassistant/components/sinch/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sinch", "codeowners": ["@bendikrb"], "requirements": ["clx-sdk-xms==1.0.0"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["clx"] } diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index cda8f0da1f235..721c51ca9644c 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -29,19 +29,15 @@ {DOMAIN: vol.Any(AUTODETECT_SCHEMA, TABLES_SCHEMA)}, extra=vol.ALLOW_EXTRA ) +# Silence these loggers by default. Their INFO level is super chatty and we +# only need error-level logging from the integration itself by default. +logging.getLogger("socketio.client").setLevel(logging.CRITICAL + 1) +logging.getLogger("engineio.client").setLevel(logging.CRITICAL + 1) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the sisyphus component.""" - class SocketIONoiseFilter(logging.Filter): - """Filters out excessively verbose logs from SocketIO.""" - - def filter(self, record): - if "waiting for connection" in record.msg: - return False - return True - - logging.getLogger("socketIO-client").addFilter(SocketIONoiseFilter()) tables = hass.data.setdefault(DATA_SISYPHUS, {}) table_configs = config[DOMAIN] session = async_get_clientsession(hass) diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index 1e0f1dc5bad46..62cfca125f6a9 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -8,5 +8,6 @@ "codeowners": [ "@jkeljo" ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["sisyphus_control"] } \ No newline at end of file diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index dccfdbe285acb..9f5fd18d53158 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": ["pyskyqhub==0.1.4"], "codeowners": ["@rogerselwyn"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyskyqhub"] } diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index da7ee08ff5989..bfca03d754f2e 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": ["pygatt[GATTTOOL]==4.0.5"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pygatt"] } diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 8b939d1d522eb..ce16617996977 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["skybellpy==0.6.3"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["skybellpy"] } diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 2605ffd2914fe..d54bb9e0ec644 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], "codeowners": ["@bachya"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["slack"] } diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 60614fcf97f01..bac88880cdba0 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,110 +1,105 @@ """Support for SleepIQ from SleepNumber.""" -from datetime import timedelta import logging -from sleepyq import Sleepyq +from asyncsleepiq import ( + AsyncSleepIQ, + SleepIQAPIException, + SleepIQLoginException, + SleepIQTimeoutException, +) import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle from .const import DOMAIN - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +from .coordinator import ( + SleepIQData, + SleepIQDataUpdateCoordinator, + SleepIQPauseUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] + CONFIG_SCHEMA = vol.Schema( { - vol.Required(DOMAIN): vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) + DOMAIN: { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } }, extra=vol.ALLOW_EXTRA, ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SleepIQ component. - - Will automatically load sensor components to support - devices discovered on the account. - """ - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - client = Sleepyq(username, password) - try: - data = SleepIQData(client) - data.update() - except ValueError: - message = """ - SleepIQ failed to login, double check your username and password" - """ - _LOGGER.error(message) - return False - - hass.data[DOMAIN] = data - discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) - discovery.load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up sleepiq component.""" + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) return True -class SleepIQData: - """Get the latest data from SleepIQ.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the SleepIQ config entry.""" + conf = entry.data + email = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] - def __init__(self, client): - """Initialize the data object.""" - self._client = client - self.beds = {} + client_session = async_get_clientsession(hass) - self.update() + gateway = AsyncSleepIQ(client_session=client_session) - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from SleepIQ.""" - self._client.login() - beds = self._client.beds_with_sleeper_status() + try: + await gateway.login(email, password) + except SleepIQLoginException: + _LOGGER.error("Could not authenticate with SleepIQ server") + return False + except SleepIQTimeoutException as err: + raise ConfigEntryNotReady( + str(err) or "Timed out during authentication" + ) from err - self.beds = {bed.bed_id: bed for bed in beds} + try: + await gateway.init_beds() + except SleepIQTimeoutException as err: + raise ConfigEntryNotReady( + str(err) or "Timed out during initialization" + ) from err + except SleepIQAPIException as err: + raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err + coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) + pause_coordinator = SleepIQPauseUpdateCoordinator(hass, gateway, email) -class SleepIQSensor(Entity): - """Implementation of a SleepIQ sensor.""" + # Call the SleepIQ API to refresh data + await coordinator.async_config_entry_first_refresh() + await pause_coordinator.async_config_entry_first_refresh() - def __init__(self, sleepiq_data, bed_id, side): - """Initialize the sensor.""" - self._bed_id = bed_id - self._side = side - self.sleepiq_data = sleepiq_data - self.side = None - self.bed = None + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SleepIQData( + data_coordinator=coordinator, + pause_coordinator=pause_coordinator, + client=gateway, + ) - # added by subclass - self._name = None - self.type = None + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - @property - def name(self): - """Return the name of the sensor.""" - return "SleepNumber {} {} {}".format( - self.bed.name, self.side.sleeper.first_name, self._name - ) + return True - def update(self): - """Get the latest data from SleepIQ and updates the states.""" - # Call the API for new sleepiq data. Each sensor will re-trigger this - # same exact call, but that's fine. We cache results for a short period - # of time to prevent hitting API limits. - self.sleepiq_data.update() - self.bed = self.sleepiq_data.beds[self._bed_id] - self.side = getattr(self.bed, self._side) +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the 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/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index f901851c0b5c2..53611edc66b93 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,60 +1,50 @@ """Support for SleepIQ sensors.""" -from __future__ import annotations +from asyncsleepiq import SleepIQBed, SleepIQSleeper from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import SleepIQSensor -from .const import DOMAIN, IS_IN_BED, SENSOR_TYPES, SIDES +from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED +from .coordinator import SleepIQData +from .entity import SleepIQSensor -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the SleepIQ sensors.""" - if discovery_info is None: - return - - data = hass.data[DOMAIN] - data.update() - - dev = [] - for bed_id, bed in data.beds.items(): - for side in SIDES: - if getattr(bed, side) is not None: - dev.append(IsInBedBinarySensor(data, bed_id, side)) - add_entities(dev) + """Set up the SleepIQ bed binary sensors.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + IsInBedBinarySensor(data.data_coordinator, bed, sleeper) + for bed in data.client.beds.values() + for sleeper in bed.sleepers + ) class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): """Implementation of a SleepIQ presence sensor.""" - def __init__(self, sleepiq_data, bed_id, side): + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, + ) -> None: """Initialize the sensor.""" - super().__init__(sleepiq_data, bed_id, side) - self._state = None - self._name = SENSOR_TYPES[IS_IN_BED] - self.update() - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state is True - - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this sensor.""" - return BinarySensorDeviceClass.OCCUPANCY - - def update(self): - """Get the latest data from SleepIQ and updates the states.""" - super().update() - self._state = self.side.is_in_bed + super().__init__(coordinator, bed, sleeper, IS_IN_BED) + + @callback + def _async_update_attrs(self) -> None: + """Update sensor attributes.""" + self._attr_is_on = self.sleeper.in_bed + self._attr_icon = ICON_OCCUPIED if self.sleeper.in_bed else ICON_EMPTY diff --git a/homeassistant/components/sleepiq/button.py b/homeassistant/components/sleepiq/button.py new file mode 100644 index 0000000000000..cca9253d58912 --- /dev/null +++ b/homeassistant/components/sleepiq/button.py @@ -0,0 +1,81 @@ +"""Support for SleepIQ buttons.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from asyncsleepiq import SleepIQBed + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQEntity + + +@dataclass +class SleepIQButtonEntityDescriptionMixin: + """Describes a SleepIQ Button entity.""" + + press_action: Callable[[SleepIQBed], Any] + + +@dataclass +class SleepIQButtonEntityDescription( + ButtonEntityDescription, SleepIQButtonEntityDescriptionMixin +): + """Class to describe a Button entity.""" + + +ENTITY_DESCRIPTIONS = [ + SleepIQButtonEntityDescription( + key="calibrate", + name="Calibrate", + press_action=lambda client: client.calibrate(), + icon="mdi:target", + ), + SleepIQButtonEntityDescription( + key="stop-pump", + name="Stop Pump", + press_action=lambda client: client.stop_pump(), + icon="mdi:stop", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sleep number buttons.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SleepNumberButton(bed, ed) + for bed in data.client.beds.values() + for ed in ENTITY_DESCRIPTIONS + ) + + +class SleepNumberButton(SleepIQEntity, ButtonEntity): + """Representation of an SleepIQ button.""" + + entity_description: SleepIQButtonEntityDescription + + def __init__( + self, bed: SleepIQBed, entity_description: SleepIQButtonEntityDescription + ) -> None: + """Initialize the Button.""" + super().__init__(bed) + self._attr_name = f"SleepNumber {bed.name} {entity_description.name}" + self._attr_unique_id = f"{bed.id}-{entity_description.key}" + self.entity_description = entity_description + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.press_action(self.bed) diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py new file mode 100644 index 0000000000000..dffb30f39d775 --- /dev/null +++ b/homeassistant/components/sleepiq/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow to configure SleepIQ component.""" +from __future__ import annotations + +from typing import Any + +from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a SleepIQ config flow.""" + + VERSION = 1 + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a SleepIQ account as a config entry. + + This flow is triggered by 'async_setup' for configured accounts. + """ + await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=import_config[CONF_USERNAME], data=import_config + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + # Don't allow multiple instances with the same username + await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + try: + await try_connection(self.hass, user_input) + except SleepIQLoginException: + errors["base"] = "invalid_auth" + except SleepIQTimeoutException: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME) + if user_input is not None + else "", + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + last_step=True, + ) + + +async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> None: + """Test if the given credentials can successfully login to SleepIQ.""" + + client_session = async_get_clientsession(hass) + + gateway = AsyncSleepIQ(client_session=client_session) + await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 64f508167e186..63e8627092523 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -1,7 +1,12 @@ """Define constants for the SleepIQ component.""" +DATA_SLEEPIQ = "data_sleepiq" DOMAIN = "sleepiq" +SLEEPYQ_INVALID_CREDENTIALS_MESSAGE = "username or password" +BED = "bed" +ICON_EMPTY = "mdi:bed-empty" +ICON_OCCUPIED = "mdi:bed" IS_IN_BED = "is_in_bed" SLEEP_NUMBER = "sleep_number" SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"} @@ -9,3 +14,6 @@ LEFT = "left" RIGHT = "right" SIDES = [LEFT, RIGHT] + +SLEEPIQ_DATA = "sleepiq_data" +SLEEPIQ_STATUS_COORDINATOR = "sleepiq_status" diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py new file mode 100644 index 0000000000000..a2394de20b190 --- /dev/null +++ b/homeassistant/components/sleepiq/coordinator.py @@ -0,0 +1,70 @@ +"""Coordinator for SleepIQ.""" +import asyncio +from dataclasses import dataclass +from datetime import timedelta +import logging + +from asyncsleepiq import AsyncSleepIQ + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(seconds=60) +LONGER_UPDATE_INTERVAL = timedelta(minutes=5) + + +class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): + """SleepIQ data update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + client: AsyncSleepIQ, + username: str, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=f"{username}@SleepIQ", + update_interval=UPDATE_INTERVAL, + ) + self.client = client + + async def _async_update_data(self) -> None: + await self.client.fetch_bed_statuses() + + +class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): + """SleepIQ data update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + client: AsyncSleepIQ, + username: str, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=f"{username}@SleepIQPause", + update_interval=LONGER_UPDATE_INTERVAL, + ) + self.client = client + + async def _async_update_data(self) -> None: + await asyncio.gather( + *[bed.fetch_pause_mode() for bed in self.client.beds.values()] + ) + + +@dataclass +class SleepIQData: + """Data for the sleepiq integration.""" + + data_coordinator: SleepIQDataUpdateCoordinator + pause_coordinator: SleepIQPauseUpdateCoordinator + client: AsyncSleepIQ diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py new file mode 100644 index 0000000000000..6d0c8784eecad --- /dev/null +++ b/homeassistant/components/sleepiq/entity.py @@ -0,0 +1,83 @@ +"""Entity for the SleepIQ integration.""" +from abc import abstractmethod + +from asyncsleepiq import SleepIQBed, SleepIQSleeper + +from homeassistant.core import callback +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ICON_OCCUPIED, SENSOR_TYPES + + +def device_from_bed(bed: SleepIQBed) -> DeviceInfo: + """Create a device given a bed.""" + return DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, bed.mac_addr)}, + manufacturer="SleepNumber", + name=bed.name, + model=bed.model, + ) + + +class SleepIQEntity(Entity): + """Implementation of a SleepIQ entity.""" + + def __init__(self, bed: SleepIQBed) -> None: + """Initialize the SleepIQ entity.""" + self.bed = bed + self._attr_device_info = device_from_bed(bed) + + +class SleepIQSensor(CoordinatorEntity): + """Implementation of a SleepIQ sensor.""" + + _attr_icon = ICON_OCCUPIED + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, + name: str, + ) -> None: + """Initialize the SleepIQ sensor entity.""" + super().__init__(coordinator) + self.sleeper = sleeper + self.bed = bed + self._attr_device_info = device_from_bed(bed) + + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" + self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + @abstractmethod + def _async_update_attrs(self) -> None: + """Update sensor attributes.""" + + +class SleepIQBedCoordinator(CoordinatorEntity): + """Implementation of a SleepIQ sensor.""" + + _attr_icon = ICON_OCCUPIED + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + ) -> None: + """Initialize the SleepIQ sensor entity.""" + super().__init__(coordinator) + self.bed = bed + self._attr_device_info = device_from_bed(bed) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index f6d4404884d86..93cd1be3204fd 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -1,8 +1,15 @@ { "domain": "sleepiq", "name": "SleepIQ", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["sleepyq==0.8.1"], - "codeowners": [], - "iot_class": "cloud_polling" + "requirements": ["asyncsleepiq==1.1.0"], + "codeowners": ["@mfugate1", "@kbickar"], + "dhcp": [ + { + "macaddress": "64DBA0*" + } + ], + "iot_class": "cloud_polling", + "loggers": ["asyncsleepiq"] } diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 523014cf8cf28..7d50876b1b2d1 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -1,62 +1,48 @@ -"""Support for SleepIQ sensors.""" +"""Support for SleepIQ Sensor.""" from __future__ import annotations +from asyncsleepiq import SleepIQBed, SleepIQSleeper + from homeassistant.components.sensor import SensorEntity -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import SleepIQSensor -from .const import DOMAIN, SENSOR_TYPES, SIDES, SLEEP_NUMBER +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -ICON = "mdi:bed" +from .const import DOMAIN, SLEEP_NUMBER +from .coordinator import SleepIQData +from .entity import SleepIQSensor -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the SleepIQ sensors.""" - if discovery_info is None: - return - - data = hass.data[DOMAIN] - data.update() - - dev = [] - for bed_id, bed in data.beds.items(): - for side in SIDES: - if getattr(bed, side) is not None: - dev.append(SleepNumberSensor(data, bed_id, side)) - add_entities(dev) - - -class SleepNumberSensor(SleepIQSensor, SensorEntity): - """Implementation of a SleepIQ sensor.""" - - def __init__(self, sleepiq_data, bed_id, side): + """Set up the SleepIQ bed sensors.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepNumberSensorEntity(data.data_coordinator, bed, sleeper) + for bed in data.client.beds.values() + for sleeper in bed.sleepers + ) + + +class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): + """Representation of an SleepIQ Entity with CoordinatorEntity.""" + + _attr_icon = "mdi:bed" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, + ) -> None: """Initialize the sensor.""" - SleepIQSensor.__init__(self, sleepiq_data, bed_id, side) - - self._state = None - self.type = SLEEP_NUMBER - self._name = SENSOR_TYPES[self.type] - - self.update() - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON + super().__init__(coordinator, bed, sleeper, SLEEP_NUMBER) - def update(self): - """Get the latest data from SleepIQ and updates the states.""" - SleepIQSensor.update(self) - self._state = self.side.sleep_number + @callback + def _async_update_attrs(self) -> None: + """Update sensor attributes.""" + self._attr_native_value = self.sleeper.sleep_number diff --git a/homeassistant/components/sleepiq/strings.json b/homeassistant/components/sleepiq/strings.json new file mode 100644 index 0000000000000..21ceead3d0a88 --- /dev/null +++ b/homeassistant/components/sleepiq/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "user": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + } + } +} diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py new file mode 100644 index 0000000000000..c8977f0ce730b --- /dev/null +++ b/homeassistant/components/sleepiq/switch.py @@ -0,0 +1,53 @@ +"""Support for SleepIQ switches.""" +from __future__ import annotations + +from typing import Any + +from asyncsleepiq import SleepIQBed + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator +from .entity import SleepIQBedCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sleep number switches.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepNumberPrivateSwitch(data.pause_coordinator, bed) + for bed in data.client.beds.values() + ) + + +class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): + """Representation of SleepIQ privacy mode.""" + + def __init__( + self, coordinator: SleepIQPauseUpdateCoordinator, bed: SleepIQBed + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator, bed) + self._attr_name = f"SleepNumber {bed.name} Pause Mode" + self._attr_unique_id = f"{bed.id}-pause-mode" + + @property + def is_on(self) -> bool: + """Return whether the switch is on or off.""" + return bool(self.bed.paused) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self.bed.set_pause_mode(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self.bed.set_pause_mode(False) diff --git a/homeassistant/components/sleepiq/translations/bg.json b/homeassistant/components/sleepiq/translations/bg.json new file mode 100644 index 0000000000000..b8fb3b61a771c --- /dev/null +++ b/homeassistant/components/sleepiq/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/ca.json b/homeassistant/components/sleepiq/translations/ca.json new file mode 100644 index 0000000000000..9c37f37a0ef56 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/cs.json b/homeassistant/components/sleepiq/translations/cs.json new file mode 100644 index 0000000000000..efbe2a91eb1cc --- /dev/null +++ b/homeassistant/components/sleepiq/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/de.json b/homeassistant/components/sleepiq/translations/de.json new file mode 100644 index 0000000000000..12a870b4cc918 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/el.json b/homeassistant/components/sleepiq/translations/el.json new file mode 100644 index 0000000000000..45b5ef57ba5b5 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/el.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "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" + }, + "step": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/en.json b/homeassistant/components/sleepiq/translations/en.json new file mode 100644 index 0000000000000..31de29c8690af --- /dev/null +++ b/homeassistant/components/sleepiq/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json new file mode 100644 index 0000000000000..6b8cd4ac64213 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/et.json b/homeassistant/components/sleepiq/translations/et.json new file mode 100644 index 0000000000000..db09683450a60 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/he.json b/homeassistant/components/sleepiq/translations/he.json new file mode 100644 index 0000000000000..49f37a267d0a6 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\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", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "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/sleepiq/translations/hu.json b/homeassistant/components/sleepiq/translations/hu.json new file mode 100644 index 0000000000000..c4adcb1bd9ec7 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/id.json b/homeassistant/components/sleepiq/translations/id.json new file mode 100644 index 0000000000000..a974f44967e4b --- /dev/null +++ b/homeassistant/components/sleepiq/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/it.json b/homeassistant/components/sleepiq/translations/it.json new file mode 100644 index 0000000000000..7ae1601843e57 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json new file mode 100644 index 0000000000000..35b6807586d9e --- /dev/null +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\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" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/nl.json b/homeassistant/components/sleepiq/translations/nl.json new file mode 100644 index 0000000000000..3271c6bce45dc --- /dev/null +++ b/homeassistant/components/sleepiq/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/no.json b/homeassistant/components/sleepiq/translations/no.json new file mode 100644 index 0000000000000..51f351fb83322 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/pl.json b/homeassistant/components/sleepiq/translations/pl.json new file mode 100644 index 0000000000000..49be6d1efde28 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/pt-BR.json b/homeassistant/components/sleepiq/translations/pt-BR.json new file mode 100644 index 0000000000000..86cf9781d3a90 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/ru.json b/homeassistant/components/sleepiq/translations/ru.json new file mode 100644 index 0000000000000..f74355cdc7d9b --- /dev/null +++ b/homeassistant/components/sleepiq/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/sk.json b/homeassistant/components/sleepiq/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/tr.json b/homeassistant/components/sleepiq/translations/tr.json new file mode 100644 index 0000000000000..153aa4126b066 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/zh-Hant.json b/homeassistant/components/sleepiq/translations/zh-Hant.json new file mode 100644 index 0000000000000..d93bfe0fa683e --- /dev/null +++ b/homeassistant/components/sleepiq/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index a360bb7491a65..324900a1d97d4 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/slide", "requirements": ["goslide-api==0.5.1"], "codeowners": ["@ualex73"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["goslideapi"] } diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index d667ab7ea3717..308c11f91a1ff 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/sma", "requirements": ["pysma==0.6.10"], "codeowners": ["@kellerza", "@rklomp"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysma"] } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 0d90d89ce985a..b06ec499a24cd 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -84,7 +84,12 @@ def __init__( @property def name(self) -> str: """Return the name of the sensor.""" - return self._sensor.name + if self._attr_device_info is None or not ( + name_prefix := self._attr_device_info.get("name") + ): + name_prefix = "SMA" + + return f"{name_prefix} {self._sensor.name}" @property def native_value(self) -> StateType: diff --git a/homeassistant/components/sma/translations/el.json b/homeassistant/components/sma/translations/el.json index 26695d002b311..b90ad093a8de5 100644 --- a/homeassistant/components/sma/translations/el.json +++ b/homeassistant/components/sma/translations/el.json @@ -1,14 +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", + "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" + }, "error": { - "cannot_retrieve_device_info": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7, \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "cannot_retrieve_device_info": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7, \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\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": { "user": { "data": { - "group": "\u039f\u03bc\u03ac\u03b4\u03b1" + "group": "\u039f\u03bc\u03ac\u03b4\u03b1", + "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", + "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", + "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" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 SMA." + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 SMA.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SMA Solar" } } } diff --git a/homeassistant/components/sma/translations/pt-BR.json b/homeassistant/components/sma/translations/pt-BR.json new file mode 100644 index 0000000000000..21017fe6fc308 --- /dev/null +++ b/homeassistant/components/sma/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "cannot_retrieve_device_info": "Conectado com sucesso, mas incapaz de recuperar as informa\u00e7\u00f5es do dispositivo", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "group": "Grupo", + "host": "Nome do host", + "password": "Senha", + "ssl": "Usar um certificado SSL", + "verify_ssl": "Verifique o certificado SSL" + }, + "description": "Insira as informa\u00e7\u00f5es do seu dispositivo SMA.", + "title": "Configurar SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/sk.json b/homeassistant/components/sma/translations/sk.json new file mode 100644 index 0000000000000..0b7bf878ea988 --- /dev/null +++ b/homeassistant/components/sma/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 6a1edaf41ae74..f27ec29996e80 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -24,5 +24,6 @@ "name": "smappee50*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["paho_mqtt", "pysmappee"] } diff --git a/homeassistant/components/smappee/translations/el.json b/homeassistant/components/smappee/translations/el.json new file mode 100644 index 0000000000000..06f797999860d --- /dev/null +++ b/homeassistant/components/smappee/translations/el.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "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", + "already_configured_local_device": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae(\u03b5\u03c2) \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7(\u03b5\u03c2). \u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c0\u03c1\u03ce\u03c4\u03b1 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c0\u03c1\u03b9\u03bd \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae cloud.", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_mdns": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Smappee.", + "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} )" + }, + "flow_title": "{name}", + "step": { + "environment": { + "data": { + "environment": "\u03a0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Smappee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." + }, + "local": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9 \u03b7 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Smappee" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "zeroconf_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Smappee \u03bc\u03b5 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 `{serialnumber}` \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Smappee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/pt-BR.json b/homeassistant/components/smappee/translations/pt-BR.json index cbaae7d2d6a41..dfd6b31fe2e47 100644 --- a/homeassistant/components/smappee/translations/pt-BR.json +++ b/homeassistant/components/smappee/translations/pt-BR.json @@ -1,13 +1,34 @@ { "config": { "abort": { - "already_configured_device": "Dispositivo j\u00e1 configurado" + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_configured_local_device": "Os dispositivos locais j\u00e1 est\u00e3o configurados. Remova-os primeiro antes de configurar um dispositivo em nuvem.", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "cannot_connect": "Falha ao conectar", + "invalid_mdns": "Dispositivo n\u00e3o suportado para a integra\u00e7\u00e3o do Smappee.", + "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})" }, + "flow_title": "{name}", "step": { + "environment": { + "data": { + "environment": "Ambiente" + }, + "description": "Configure seu Smappee para integrar com o Home Assistant." + }, "local": { "data": { - "host": "Host" - } + "host": "Nome do host" + }, + "description": "Digite o host para iniciar a integra\u00e7\u00e3o local do Smappee" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "zeroconf_confirm": { + "description": "Deseja adicionar o dispositivo Smappee com n\u00famero de s\u00e9rie ` {serialnumber} ` ao Home Assistant?", + "title": "Dispositivo Smappee descoberto" } } } diff --git a/homeassistant/components/smart_meter_texas/manifest.json b/homeassistant/components/smart_meter_texas/manifest.json index f70cf59b9b9cf..2a65de9ed115c 100644 --- a/homeassistant/components/smart_meter_texas/manifest.json +++ b/homeassistant/components/smart_meter_texas/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/smart_meter_texas", "requirements": ["smart-meter-texas==0.4.7"], "codeowners": ["@grahamwetzler"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["smart_meter_texas"] } diff --git a/homeassistant/components/smart_meter_texas/translations/el.json b/homeassistant/components/smart_meter_texas/translations/el.json new file mode 100644 index 0000000000000..5b1861a0e4080 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/el.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/pt-BR.json b/homeassistant/components/smart_meter_texas/translations/pt-BR.json new file mode 100644 index 0000000000000..66c671f99a3c2 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ 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 new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 054aaca2d7678..7974215de64aa 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "requirements": ["smarthab==0.21"], "codeowners": ["@outadoc"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pysmarthab"] } diff --git a/homeassistant/components/smarthab/translations/el.json b/homeassistant/components/smarthab/translations/el.json new file mode 100644 index 0000000000000..43de6c1ca8902 --- /dev/null +++ b/homeassistant/components/smarthab/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "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", + "service": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf SmartHab. \u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0393\u03b9\u03b1 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03bf\u03cd\u03c2 \u03bb\u03cc\u03b3\u03bf\u03c5\u03c2, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03b5\u03b9\u03b4\u03b9\u03ba\u03ac \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SmartHab.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SmartHab" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pt-BR.json b/homeassistant/components/smarthab/translations/pt-BR.json index e1f66450b015f..ef205c53827d2 100644 --- a/homeassistant/components/smarthab/translations/pt-BR.json +++ b/homeassistant/components/smarthab/translations/pt-BR.json @@ -1,8 +1,19 @@ { "config": { "error": { - "invalid_auth": "Autentica\u00e7\u00e3o invalida", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "service": "Erro ao tentar acessar o SmartHab. O servi\u00e7o pode estar inoperante. Verifique sua conex\u00e3o.", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "description": "Por motivos t\u00e9cnicos, certifique-se de usar uma conta secund\u00e1ria espec\u00edfica para a configura\u00e7\u00e3o do Home Assistant. Voc\u00ea pode criar um a partir do aplicativo SmartHab.", + "title": "Configurar SmartHab" + } } } } \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/sk.json b/homeassistant/components/smarthab/translations/sk.json new file mode 100644 index 0000000000000..72b0304f1c3bd --- /dev/null +++ b/homeassistant/components/smarthab/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index b67a05d57537d..b4a043e2f1361 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -29,5 +29,6 @@ "hostname": "hub*", "macaddress": "286D97*" } - ] + ], + "loggers": ["httpsig", "pysmartapp", "pysmartthings"] } diff --git a/homeassistant/components/smartthings/translations/el.json b/homeassistant/components/smartthings/translations/el.json index 4541308b52614..ea4c841cd13b7 100644 --- a/homeassistant/components/smartthings/translations/el.json +++ b/homeassistant/components/smartthings/translations/el.json @@ -1,19 +1,37 @@ { "config": { + "abort": { + "invalid_webhook_url": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c9\u03c3\u03c4\u03ac \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf SmartThings. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 webhook \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7:\n > {webhook_url} \n\n \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]( {component_url} ), \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "no_available_locations": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 SmartThings \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf Home Assistant." + }, "error": { + "app_setup_error": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 SmartApp. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "token_forbidden": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c0\u03b5\u03b4\u03af\u03b1 OAuth.", + "token_invalid_format": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae UID/GUID", + "token_unauthorized": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf.", "webhook_error": "\u03a4\u03bf SmartThings \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03ba\u03c5\u03c1\u03ce\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 webhook. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 webhook \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "step": { + "authorize": { + "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Home Assistant" + }, "pat": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 SmartThings [Personal Access Token]({token_url}) \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]({component_url}). \u0391\u03c5\u03c4\u03cc \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 SmartThings.", "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "select_location": { + "data": { + "location_id": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" + }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 SmartThings \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b8\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1.", "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" }, "user": { - "description": "\u03a4\u03bf SmartThings \u03b8\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 push \u03c3\u03c4\u03bf Home Assistant \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7:\n> {webhook_url}\n\n\u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc, \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2, \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + "description": "\u03a4\u03bf SmartThings \u03b8\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 push \u03c3\u03c4\u03bf Home Assistant \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7:\n> {webhook_url}\n\n\u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc, \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2, \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 URL \u03b5\u03c0\u03b1\u03bd\u03ac\u03ba\u03bb\u03b7\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/smartthings/translations/pt-BR.json b/homeassistant/components/smartthings/translations/pt-BR.json index 5b6329a530fbe..a4a80fb29cca4 100644 --- a/homeassistant/components/smartthings/translations/pt-BR.json +++ b/homeassistant/components/smartthings/translations/pt-BR.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_webhook_url": "O Home Assistant n\u00e3o est\u00e1 configurado corretamente para receber atualiza\u00e7\u00f5es do SmartThings. O URL do webhook \u00e9 inv\u00e1lido:\n> {webhook_url}\n\nAtualize sua configura\u00e7\u00e3o de acordo com as [instru\u00e7\u00f5es]({component_url}), reinicie o Home Assistant e tente novamente.", "no_available_locations": "N\u00e3o h\u00e1 Locais SmartThings dispon\u00edveis para configura\u00e7\u00e3o no Home Assistant." }, "error": { @@ -8,12 +9,29 @@ "token_forbidden": "O token n\u00e3o possui os escopos necess\u00e1rios do OAuth.", "token_invalid_format": "O token deve estar no formato UID / GUID", "token_unauthorized": "O token \u00e9 inv\u00e1lido ou n\u00e3o est\u00e1 mais autorizado.", - "webhook_error": "O SmartThings n\u00e3o p\u00f4de validar o terminal configurado em `base_url`. Por favor, revise os requisitos do componente." + "webhook_error": "SmartThings n\u00e3o p\u00f4de validar a URL do webhook. Por favor, certifique-se de que a URL do webhook seja acess\u00edvel a partir da internet e tente novamente." }, "step": { + "authorize": { + "title": "Autorizar o Home Assistant" + }, + "pat": { + "data": { + "access_token": "Token de acesso" + }, + "description": "Insira um [Token de Acesso Pessoal]({token_url}) SmartThings que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}). Isso ser\u00e1 usado para criar a integra\u00e7\u00e3o do Home Assistant em sua conta SmartThings.", + "title": "Insira o token de acesso pessoal" + }, + "select_location": { + "data": { + "location_id": "Localiza\u00e7\u00e3o" + }, + "description": "Selecione o local do SmartThings que deseja adicionar ao Home Assistant. Em seguida, abriremos uma nova janela e solicitaremos que voc\u00ea fa\u00e7a login e autorize a instala\u00e7\u00e3o da integra\u00e7\u00e3o do Home Assistant no local selecionado.", + "title": "Selecione a localiza\u00e7\u00e3o" + }, "user": { - "description": "Por favor, insira um SmartThings [Personal Access Token] ( {token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es] ( {component_url} ).", - "title": "Digite o token de acesso pessoal" + "description": "SmartThings ser\u00e1 configurado para enviar atualiza\u00e7\u00f5es push para home assistant em:\n> {webhook_url}\n\nSe isso n\u00e3o estiver correto, atualize sua configura\u00e7\u00e3o, reinicie o Home Assistant e tente novamente.", + "title": "Confirmar URL de retorno de chamada" } } } diff --git a/homeassistant/components/smartthings/translations/sk.json b/homeassistant/components/smartthings/translations/sk.json new file mode 100644 index 0000000000000..534c1e859ee6e --- /dev/null +++ b/homeassistant/components/smartthings/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "pat": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token" + } + }, + "select_location": { + "data": { + "location_id": "Umiestnenie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 7d9a963b26c8c..9bec5d4a72e7b 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@mdz"], "requirements": ["python-smarttub==0.0.29"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["smarttub"] } diff --git a/homeassistant/components/smarttub/translations/el.json b/homeassistant/components/smarttub/translations/el.json index bf55da881cecb..4f6e62a880c82 100644 --- a/homeassistant/components/smarttub/translations/el.json +++ b/homeassistant/components/smarttub/translations/el.json @@ -1,10 +1,22 @@ { "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", + "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_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_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SmartTub \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2" + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SmartTub \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \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" }, "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 email \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf SmartTub \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7" } diff --git a/homeassistant/components/smarttub/translations/pt-BR.json b/homeassistant/components/smarttub/translations/pt-BR.json new file mode 100644 index 0000000000000..4eedb6d7a3bbe --- /dev/null +++ b/homeassistant/components/smarttub/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do SmartTub precisa re-autenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "email": "Email", + "password": "Senha" + }, + "description": "Digite seu endere\u00e7o de e-mail e senha do SmartTub para fazer login", + "title": "Login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/sk.json b/homeassistant/components/smarttub/translations/sk.json new file mode 100644 index 0000000000000..f8b6dfeea813e --- /dev/null +++ b/homeassistant/components/smarttub/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index cfae1d98a5b96..734e1a44dcfee 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/smarty", "requirements": ["pysmarty==0.8"], "codeowners": ["@z0mbieprocess"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymodbus", "pysmarty"] } diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 4eedc28d37858..d1030cb786864 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/smhi", "requirements": ["smhi-pkg==1.0.15"], "codeowners": ["@gjohansson-ST"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["smhi"] } diff --git a/homeassistant/components/smhi/translations/el.json b/homeassistant/components/smhi/translations/el.json index d5323c2c07495..c852bd3a08e0d 100644 --- a/homeassistant/components/smhi/translations/el.json +++ b/homeassistant/components/smhi/translations/el.json @@ -4,10 +4,16 @@ "already_configured": "\u039f \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2" }, "error": { - "name_exists": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" + "name_exists": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7", + "wrong_location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u039c\u03cc\u03bd\u03bf \u03a3\u03bf\u03c5\u03b7\u03b4\u03af\u03b1" }, "step": { "user": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, "title": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c3\u03c4\u03b7 \u03a3\u03bf\u03c5\u03b7\u03b4\u03af\u03b1" } } diff --git a/homeassistant/components/smhi/translations/fr.json b/homeassistant/components/smhi/translations/fr.json index ec5350903487e..6bf27915f97c8 100644 --- a/homeassistant/components/smhi/translations/fr.json +++ b/homeassistant/components/smhi/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, "error": { "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9", "wrong_location": "En Su\u00e8de uniquement" diff --git a/homeassistant/components/smhi/translations/pl.json b/homeassistant/components/smhi/translations/pl.json index f1b30177d5a2c..18e9156a936be 100644 --- a/homeassistant/components/smhi/translations/pl.json +++ b/homeassistant/components/smhi/translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, "error": { "name_exists": "Nazwa ju\u017c istnieje", "wrong_location": "Lokalizacja w Szwecji" diff --git a/homeassistant/components/smhi/translations/pt-BR.json b/homeassistant/components/smhi/translations/pt-BR.json index 0bc966fdd6cb9..235008c7c31a1 100644 --- a/homeassistant/components/smhi/translations/pt-BR.json +++ b/homeassistant/components/smhi/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, "error": { "name_exists": "O nome j\u00e1 existe", "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" diff --git a/homeassistant/components/smhi/translations/sk.json b/homeassistant/components/smhi/translations/sk.json new file mode 100644 index 0000000000000..81532ef480193 --- /dev/null +++ b/homeassistant/components/smhi/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index 6d736ac44e750..d98304ebf238f 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/sms", "requirements": ["python-gammu==3.2.3"], "codeowners": ["@ocalvo"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["gammu"] } diff --git a/homeassistant/components/sms/translations/el.json b/homeassistant/components/sms/translations/el.json new file mode 100644 index 0000000000000..372cdfed20f38 --- /dev/null +++ b/homeassistant/components/sms/translations/el.json @@ -0,0 +1,20 @@ +{ + "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", + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/it.json b/homeassistant/components/sms/translations/it.json index 77098f93470ab..9d2ac87d83318 100644 --- a/homeassistant/components/sms/translations/it.json +++ b/homeassistant/components/sms/translations/it.json @@ -13,7 +13,7 @@ "data": { "device": "Dispositivo" }, - "title": "Connettersi al modem" + "title": "Connettiti al modem" } } } diff --git a/homeassistant/components/sms/translations/pt-BR.json b/homeassistant/components/sms/translations/pt-BR.json new file mode 100644 index 0000000000000..05e211760f251 --- /dev/null +++ b/homeassistant/components/sms/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "device": "Dispositivo" + }, + "title": "Conectado ao modem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/uk.json b/homeassistant/components/sms/translations/uk.json index be271a2b6e489..0105742da5fe1 100644 --- a/homeassistant/components/sms/translations/uk.json +++ b/homeassistant/components/sms/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/sms/translations/zh-Hant.json b/homeassistant/components/sms/translations/zh-Hant.json index 12cfbc75384ba..b6e08ffa7ec27 100644 --- a/homeassistant/components/sms/translations/zh-Hant.json +++ b/homeassistant/components/sms/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 2e3249f4551d7..675a60e40969e 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/snapcast", "requirements": ["snapcast==2.1.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["construct", "snapcast"] } diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 19cd258ce6ffc..76df9e186060f 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": ["pysnmp==4.4.12"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyasn1", "pysmi", "pysnmp"] } diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index e270e81012230..6ff42bd480088 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sochain", "requirements": ["python-sochain-api==0.0.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pysochain"] } diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 84b1e6b9445bd..e5c9520f96b84 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -11,5 +11,6 @@ "macaddress": "002702*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["solaredge"] } diff --git a/homeassistant/components/solaredge/translations/el.json b/homeassistant/components/solaredge/translations/el.json index d8fe133bff448..7b460c0571765 100644 --- a/homeassistant/components/solaredge/translations/el.json +++ b/homeassistant/components/solaredge/translations/el.json @@ -1,7 +1,21 @@ { "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": { + "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", + "could_not_connect": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf API \u03c4\u03bf\u03c5 solarage", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "site_not_active": "\u039f \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2" + }, "step": { "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "name": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "site_id": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 SolarEdge" + }, "title": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 API \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" } } diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index 6d41904836ab4..f89c85eeaee5a 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "could_not_connect": "Impossibile connettersi all'API Solaredge", + "could_not_connect": "Impossibile connettersi all'API solaredge", "invalid_api_key": "Chiave API non valida", "site_not_active": "Il sito non \u00e8 attivo" }, diff --git a/homeassistant/components/solaredge/translations/pt-BR.json b/homeassistant/components/solaredge/translations/pt-BR.json new file mode 100644 index 0000000000000..f005c2913ef6a --- /dev/null +++ b/homeassistant/components/solaredge/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "could_not_connect": "N\u00e3o foi poss\u00edvel conectar-se \u00e0 API do solaredge", + "invalid_api_key": "Chave de API inv\u00e1lida", + "site_not_active": "O site n\u00e3o est\u00e1 ativo" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "name": "O nome desta instala\u00e7\u00e3o", + "site_id": "O ID do site SolarEdge" + }, + "title": "Defina os par\u00e2metros da API para esta instala\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/sk.json b/homeassistant/components/solaredge/translations/sk.json new file mode 100644 index 0000000000000..7c6659c2e0b42 --- /dev/null +++ b/homeassistant/components/solaredge/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 56e722174b4f3..02f21c69fea40 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge_local", "requirements": ["solaredge-local==0.2.0"], "codeowners": ["@drobtravels", "@scheric"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["solaredge_local"] } diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 5535da860f0bb..5d67ed6bf89d1 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/solarlog", "codeowners": ["@Ernst79"], "requirements": ["sunwatcher==0.2.1"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["sunwatcher"] } diff --git a/homeassistant/components/solarlog/translations/el.json b/homeassistant/components/solarlog/translations/el.json new file mode 100644 index 0000000000000..320ea67bf9819 --- /dev/null +++ b/homeassistant/components/solarlog/translations/el.json @@ -0,0 +1,20 @@ +{ + "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": { + "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" + }, + "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": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 Solar-Log" + }, + "title": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/pt-BR.json b/homeassistant/components/solarlog/translations/pt-BR.json new file mode 100644 index 0000000000000..b935d245da9e7 --- /dev/null +++ b/homeassistant/components/solarlog/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "O prefixo a ser usado para seus sensores Solar-Log" + }, + "title": "Defina sua conex\u00e3o Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/const.py b/homeassistant/components/solax/const.py index bf8abe19af162..65894013adc41 100644 --- a/homeassistant/components/solax/const.py +++ b/homeassistant/components/solax/const.py @@ -2,3 +2,5 @@ DOMAIN = "solax" + +MANUFACTURER = "SolaX Power" diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index e8a905ca8bc3f..17ae6db0232df 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -5,5 +5,6 @@ "requirements": ["solax==0.2.9"], "codeowners": ["@squishykid"], "iot_class": "local_polling", - "config_flow": true + "config_flow": true, + "loggers": ["solax"] } diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 83c1ace569dcf..6f1b5ef6cf3e4 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -19,11 +19,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -46,6 +47,7 @@ async def async_setup_entry( api = hass.data[DOMAIN][entry.entry_id] resp = await api.get_data() serial = resp.serial_number + version = resp.version endpoint = RealTimeDataEndpoint(hass, api) hass.async_add_job(endpoint.async_refresh) async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) @@ -72,7 +74,9 @@ async def async_setup_entry( device_class = SensorDeviceClass.BATTERY state_class = SensorStateClass.MEASUREMENT uid = f"{serial}-{idx}" - devices.append(Inverter(uid, serial, sensor, unit, state_class, device_class)) + devices.append( + Inverter(uid, serial, version, sensor, unit, state_class, device_class) + ) endpoint.sensors = devices async_add_entities(devices) @@ -140,6 +144,7 @@ def __init__( self, uid, serial, + version, key, unit, state_class=None, @@ -151,6 +156,12 @@ def __init__( self._attr_native_unit_of_measurement = unit self._attr_state_class = state_class self._attr_device_class = device_class + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial)}, + manufacturer=MANUFACTURER, + name=f"Solax {serial}", + sw_version=version, + ) self.key = key self.value = None diff --git a/homeassistant/components/solax/translations/bg.json b/homeassistant/components/solax/translations/bg.json new file mode 100644 index 0000000000000..d6778a65bc7e1 --- /dev/null +++ b/homeassistant/components/solax/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/ca.json b/homeassistant/components/solax/translations/ca.json new file mode 100644 index 0000000000000..7f7ce67da3997 --- /dev/null +++ b/homeassistant/components/solax/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "password": "Contrasenya", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/cs.json b/homeassistant/components/solax/translations/cs.json new file mode 100644 index 0000000000000..7940c6378feca --- /dev/null +++ b/homeassistant/components/solax/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/de.json b/homeassistant/components/solax/translations/de.json new file mode 100644 index 0000000000000..45c8092377700 --- /dev/null +++ b/homeassistant/components/solax/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse", + "password": "Passwort", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/el.json b/homeassistant/components/solax/translations/el.json new file mode 100644 index 0000000000000..1ed862e8f846c --- /dev/null +++ b/homeassistant/components/solax/translations/el.json @@ -0,0 +1,17 @@ +{ + "config": { + "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": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/et.json b/homeassistant/components/solax/translations/et.json new file mode 100644 index 0000000000000..e89f90a61191c --- /dev/null +++ b/homeassistant/components/solax/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress", + "password": "Salas\u00f5na", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/fr.json b/homeassistant/components/solax/translations/fr.json new file mode 100644 index 0000000000000..a0d7a14dcdb50 --- /dev/null +++ b/homeassistant/components/solax/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossible de se connecter", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP", + "password": "Mot de passe", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/he.json b/homeassistant/components/solax/translations/he.json new file mode 100644 index 0000000000000..44e903b122022 --- /dev/null +++ b/homeassistant/components/solax/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "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": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/hu.json b/homeassistant/components/solax/translations/hu.json new file mode 100644 index 0000000000000..69dfcc5d841f1 --- /dev/null +++ b/homeassistant/components/solax/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "password": "Jelsz\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/id.json b/homeassistant/components/solax/translations/id.json new file mode 100644 index 0000000000000..b1f864ad85ba3 --- /dev/null +++ b/homeassistant/components/solax/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "password": "Kata Sandi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/it.json b/homeassistant/components/solax/translations/it.json new file mode 100644 index 0000000000000..e4fa82ddf4677 --- /dev/null +++ b/homeassistant/components/solax/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "password": "Password", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/ja.json b/homeassistant/components/solax/translations/ja.json new file mode 100644 index 0000000000000..70aee01ce0f2a --- /dev/null +++ b/homeassistant/components/solax/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/nb.json b/homeassistant/components/solax/translations/nb.json new file mode 100644 index 0000000000000..66b7784c3fc7c --- /dev/null +++ b/homeassistant/components/solax/translations/nb.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP-adresse", + "password": "Passord", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/nl.json b/homeassistant/components/solax/translations/nl.json new file mode 100644 index 0000000000000..4e0c400148f99 --- /dev/null +++ b/homeassistant/components/solax/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres", + "password": "Wachtwoord", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/no.json b/homeassistant/components/solax/translations/no.json new file mode 100644 index 0000000000000..8e82b60bce1ea --- /dev/null +++ b/homeassistant/components/solax/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "password": "Passord", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/pl.json b/homeassistant/components/solax/translations/pl.json new file mode 100644 index 0000000000000..e92874775ada2 --- /dev/null +++ b/homeassistant/components/solax/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP", + "password": "Has\u0142o", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/pt-BR.json b/homeassistant/components/solax/translations/pt-BR.json new file mode 100644 index 0000000000000..b000a3b07c389 --- /dev/null +++ b/homeassistant/components/solax/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "password": "Senha", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/ru.json b/homeassistant/components/solax/translations/ru.json new file mode 100644 index 0000000000000..c05b4b56bc4f2 --- /dev/null +++ b/homeassistant/components/solax/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/sk.json b/homeassistant/components/solax/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/solax/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/sv.json b/homeassistant/components/solax/translations/sv.json new file mode 100644 index 0000000000000..a43a8312e99a2 --- /dev/null +++ b/homeassistant/components/solax/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adress", + "password": "L\u00f6senord", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/tr.json b/homeassistant/components/solax/translations/tr.json new file mode 100644 index 0000000000000..54b4b67c5e7cb --- /dev/null +++ b/homeassistant/components/solax/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi", + "password": "Parola", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/uk.json b/homeassistant/components/solax/translations/uk.json new file mode 100644 index 0000000000000..bc7d29b12f9e4 --- /dev/null +++ b/homeassistant/components/solax/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/zh-Hant.json b/homeassistant/components/solax/translations/zh-Hant.json new file mode 100644 index 0000000000000..7896b5796edcb --- /dev/null +++ b/homeassistant/components/solax/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 1bde431e9d7f5..88d77b775c5bc 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -8,5 +8,6 @@ "@sebfortier2288" ], "requirements": ["pysoma==0.0.10"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["api"] } diff --git a/homeassistant/components/soma/translations/el.json b/homeassistant/components/soma/translations/el.json new file mode 100644 index 0000000000000..e393d8ffb6568 --- /dev/null +++ b/homeassistant/components/soma/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "\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.", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf Soma \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.", + "result_error": "\u03a4\u03bf SOMA Connect \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b5 \u03bc\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\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", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "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 SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/pt-BR.json b/homeassistant/components/soma/translations/pt-BR.json index 3485b5304b466..fb1af9db783f2 100644 --- a/homeassistant/components/soma/translations/pt-BR.json +++ b/homeassistant/components/soma/translations/pt-BR.json @@ -1,15 +1,23 @@ { "config": { "abort": { - "connection_error": "Falha na liga\u00e7\u00e3o \u00e0 SOMA Connect.", + "already_setup": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "connection_error": "Falha ao conectar", + "missing_configuration": "O componente Soma n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "result_error": "SOMA Connect respondeu com status de erro." }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" - } + }, + "description": "Insira as configura\u00e7\u00f5es de conex\u00e3o do seu SOMA Connect.", + "title": "SOMA Connect" } } } diff --git a/homeassistant/components/soma/translations/sk.json b/homeassistant/components/soma/translations/sk.json new file mode 100644 index 0000000000000..91a46a2787f8c --- /dev/null +++ b/homeassistant/components/soma/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/zh-Hant.json b/homeassistant/components/soma/translations/zh-Hant.json index c4e2796e189e2..c3ad4dc1bf1af 100644 --- a/homeassistant/components/soma/translations/zh-Hant.json +++ b/homeassistant/components/soma/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "already_setup": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "connection_error": "\u9023\u7dda\u5931\u6557", "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 1adbab49fb2c4..144938b18225b 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -12,5 +12,6 @@ "name": "gateway*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pymfy"] } diff --git a/homeassistant/components/somfy/translations/el.json b/homeassistant/components/somfy/translations/el.json index aecb2ee553fa4..8d1f457ae10fa 100644 --- a/homeassistant/components/somfy/translations/el.json +++ b/homeassistant/components/somfy/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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} )", "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } } } } \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/pt-BR.json b/homeassistant/components/somfy/translations/pt-BR.json index 2f3377428707c..8ad5fac904490 100644 --- a/homeassistant/components/somfy/translations/pt-BR.json +++ b/homeassistant/components/somfy/translations/pt-BR.json @@ -1,11 +1,18 @@ { "config": { "abort": { - "authorize_url_timeout": "Excedido tempo limite gerando a URL de autoriza\u00e7\u00e3o.", - "missing_configuration": "O componente Somfy n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { - "default": "Autenticado com sucesso pela Somfy." + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } } } } \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/sk.json b/homeassistant/components/somfy/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/somfy/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json index ebf7e41044eef..207169ad6b0ae 100644 --- a/homeassistant/components/somfy/translations/uk.json +++ b/homeassistant/components/somfy/translations/uk.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "create_entry": { "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json index 71390930e358a..8dccd6771cb13 100644 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ b/homeassistant/components/somfy/translations/zh-Hant.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "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})", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index fd036daf385cd..a7fb40eafd49b 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index a376654ede4ae..26d56416e644d 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -11,5 +11,6 @@ "macaddress": "B8B7F1*" } ], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["somfy_mylink_synergy"] } diff --git a/homeassistant/components/somfy_mylink/translations/el.json b/homeassistant/components/somfy_mylink/translations/el.json index 29a6b53d5506f..59ffb56368703 100644 --- a/homeassistant/components/somfy_mylink/translations/el.json +++ b/homeassistant/components/somfy_mylink/translations/el.json @@ -1,13 +1,53 @@ { "config": { - "flow_title": "{mac} ({ip})" + "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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "system_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + }, + "description": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae MyLink \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u0395\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03c2 \u03bf\u03c0\u03bf\u03b9\u03b1\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 Cloud." + } + } }, "options": { + "abort": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { + "entity_config": { + "data": { + "reverse": "\u03a4\u03bf \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b5\u03c3\u03c4\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf" + }, + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 `{entity_id}`", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "init": { + "data": { + "default_reverse": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b1 \u03ba\u03b1\u03bb\u03cd\u03bc\u03bc\u03b1\u03c4\u03b1", + "entity_id": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2.", + "target_id": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1." + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd MyLink" + }, "target_config": { + "data": { + "reverse": "\u03a4\u03bf \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03c3\u03c4\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf" + }, "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf `{target_name}`", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 MyLink Cover" } } - } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/pt-BR.json b/homeassistant/components/somfy_mylink/translations/pt-BR.json new file mode 100644 index 0000000000000..12efd378984f2 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pt-BR.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Nome do host", + "port": "Porta", + "system_id": "ID do sistema" + }, + "description": "A ID do sistema pode ser obtida no aplicativo MyLink em Integra\u00e7\u00e3o selecionando qualquer servi\u00e7o n\u00e3o Cloud." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "entity_config": { + "data": { + "reverse": "A cobertura est\u00e1 invertida" + }, + "description": "Configurar op\u00e7\u00f5es para ` {entity_id} `", + "title": "Configurar entidade" + }, + "init": { + "data": { + "default_reverse": "Status de revers\u00e3o padr\u00e3o para coberturas n\u00e3o configuradas", + "entity_id": "Configure uma entidade espec\u00edfica.", + "target_id": "Configure as op\u00e7\u00f5es para uma cobertura." + }, + "title": "Configurar op\u00e7\u00f5es do MyLink" + }, + "target_config": { + "data": { + "reverse": "A cobertura est\u00e1 invertida" + }, + "description": "Configurar op\u00e7\u00f5es para ` {target_name} `", + "title": "Configurar a MyLink Cover" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/sk.json b/homeassistant/components/somfy_mylink/translations/sk.json new file mode 100644 index 0000000000000..1145b3bb9f844 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index b574e68ae2fe0..9934cc8f481da 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -2,8 +2,11 @@ from __future__ import annotations from datetime import timedelta +import logging -from sonarr import Sonarr, SonarrAccessRestricted, SonarrError +from aiopyarr import ArrAuthenticationException, ArrException +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.sonarr_client import SonarrClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -11,6 +14,7 @@ CONF_HOST, CONF_PORT, CONF_SSL, + CONF_URL, CONF_VERIFY_SSL, Platform, ) @@ -22,7 +26,9 @@ CONF_BASE_PATH, CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, + DATA_HOST_CONFIG, DATA_SONARR, + DATA_SYSTEM_STATUS, DEFAULT_UPCOMING_DAYS, DEFAULT_WANTED_MAX_ITEMS, DOMAIN, @@ -30,6 +36,7 @@ PLATFORMS = [Platform.SENSOR] SCAN_INTERVAL = timedelta(seconds=30) +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -45,30 +52,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } hass.config_entries.async_update_entry(entry, options=options) - sonarr = Sonarr( - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - api_key=entry.data[CONF_API_KEY], - base_path=entry.data[CONF_BASE_PATH], - session=async_get_clientsession(hass), - tls=entry.data[CONF_SSL], + host_configuration = PyArrHostConfiguration( + api_token=entry.data[CONF_API_KEY], + url=entry.data[CONF_URL], verify_ssl=entry.data[CONF_VERIFY_SSL], ) + sonarr = SonarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass), + ) + try: - await sonarr.update() - except SonarrAccessRestricted as err: + system_status = await sonarr.async_get_system_status() + except ArrAuthenticationException as err: raise ConfigEntryAuthFailed( "API Key is no longer valid. Please reauthenticate" ) from err - except SonarrError as err: + except ArrException as err: raise ConfigEntryNotReady from err entry.async_on_unload(entry.add_update_listener(_async_update_listener)) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { + DATA_HOST_CONFIG: host_configuration, DATA_SONARR: sonarr, + DATA_SYSTEM_STATUS: system_status, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -76,6 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + new_proto = "https" if entry.data[CONF_SSL] else "http" + new_host_port = f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + + new_path = "" + + if entry.data[CONF_BASE_PATH].rstrip("/") not in ("", "/", "/api"): + new_path = entry.data[CONF_BASE_PATH].rstrip("/") + + data = { + **entry.data, + CONF_URL: f"{new_proto}://{new_host_port}{new_path}", + } + hass.config_entries.async_update_entry(entry, data=data) + entry.version = 2 + + _LOGGER.info("Migration to version %s successful", entry.version) + + 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) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index f226d1883a5ff..c9ef2d3ecc8c8 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -4,28 +4,21 @@ import logging from typing import Any -from sonarr import Sonarr, SonarrAccessRestricted, SonarrError +from aiopyarr import ArrAuthenticationException, ArrException +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.sonarr_client import SonarrClient import voluptuous as vol +import yarl from homeassistant.config_entries import ConfigFlow, OptionsFlow -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_BASE_PATH, CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, - DEFAULT_BASE_PATH, - DEFAULT_PORT, - DEFAULT_SSL, DEFAULT_UPCOMING_DAYS, DEFAULT_VERIFY_SSL, DEFAULT_WANTED_MAX_ITEMS, @@ -40,25 +33,24 @@ async def validate_input(hass: HomeAssistant, data: dict) -> None: Data has the keys from DATA_SCHEMA with values provided by the user. """ - session = async_get_clientsession(hass) - - sonarr = Sonarr( - host=data[CONF_HOST], - port=data[CONF_PORT], - api_key=data[CONF_API_KEY], - base_path=data[CONF_BASE_PATH], - tls=data[CONF_SSL], + host_configuration = PyArrHostConfiguration( + api_token=data[CONF_API_KEY], + url=data[CONF_URL], verify_ssl=data[CONF_VERIFY_SSL], - session=session, ) - await sonarr.update() + sonarr = SonarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass), + ) + + await sonarr.async_get_system_status() class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sonarr.""" - VERSION = 1 + VERSION = 2 def __init__(self): """Initialize the flow.""" @@ -83,7 +75,7 @@ async def async_step_reauth_confirm( if user_input is None: return self.async_show_form( step_id="reauth_confirm", - description_placeholders={"host": self.entry.data[CONF_HOST]}, + description_placeholders={"url": self.entry.data[CONF_URL]}, data_schema=vol.Schema({}), errors={}, ) @@ -105,9 +97,9 @@ async def async_step_user( try: await validate_input(self.hass, user_input) - except SonarrAccessRestricted: + except ArrAuthenticationException: errors = {"base": "invalid_auth"} - except SonarrError: + except ArrException: errors = {"base": "cannot_connect"} except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -116,8 +108,10 @@ async def async_step_user( if self.entry: return await self._async_reauth_update_entry(user_input) + parsed = yarl.URL(user_input[CONF_URL]) + return self.async_create_entry( - title=user_input[CONF_HOST], data=user_input + title=parsed.host or "Sonarr", data=user_input ) data_schema = self._get_user_data_schema() @@ -139,12 +133,9 @@ def _get_user_data_schema(self) -> dict[str, Any]: if self.entry: return {vol.Required(CONF_API_KEY): str} - data_schema = { - vol.Required(CONF_HOST): str, + data_schema: dict[str, Any] = { + vol.Required(CONF_URL): str, vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_BASE_PATH, default=DEFAULT_BASE_PATH): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, } if self.show_advanced_options: diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py index be0fa00d597dd..58f5c4657168a 100644 --- a/homeassistant/components/sonarr/const.py +++ b/homeassistant/components/sonarr/const.py @@ -7,17 +7,14 @@ CONF_INCLUDED = "include_paths" CONF_UNIT = "unit" CONF_UPCOMING_DAYS = "upcoming_days" -CONF_URLBASE = "urlbase" CONF_WANTED_MAX_ITEMS = "wanted_max_items" # Data +DATA_HOST_CONFIG = "host_config" DATA_SONARR = "sonarr" +DATA_SYSTEM_STATUS = "system_status" # Defaults -DEFAULT_BASE_PATH = "/api" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 8989 -DEFAULT_SSL = False DEFAULT_UPCOMING_DAYS = 1 DEFAULT_VERIFY_SSL = False DEFAULT_WANTED_MAX_ITEMS = 50 diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 1d0cb2ce6f3bf..41f6786503d17 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -1,7 +1,9 @@ """Base Entity for Sonarr.""" from __future__ import annotations -from sonarr import Sonarr +from aiopyarr import SystemStatus +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.sonarr_client import SonarrClient from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity @@ -15,7 +17,9 @@ class SonarrEntity(Entity): def __init__( self, *, - sonarr: Sonarr, + sonarr: SonarrClient, + host_config: PyArrHostConfiguration, + system_status: SystemStatus, entry_id: str, device_id: str, ) -> None: @@ -23,6 +27,8 @@ def __init__( self._entry_id = entry_id self._device_id = device_id self.sonarr = sonarr + self.host_config = host_config + self.system_status = system_status @property def device_info(self) -> DeviceInfo | None: @@ -30,15 +36,11 @@ def device_info(self) -> DeviceInfo | None: if self._device_id is None: return None - configuration_url = "https://" if self.sonarr.tls else "http://" - configuration_url += f"{self.sonarr.host}:{self.sonarr.port}" - configuration_url += self.sonarr.base_path.replace("/api", "") - return DeviceInfo( identifiers={(DOMAIN, self._device_id)}, name="Activity Sensor", manufacturer="Sonarr", - sw_version=self.sonarr.app.info.version, + sw_version=self.system_status.version, entry_type=DeviceEntryType.SERVICE, - configuration_url=configuration_url, + configuration_url=self.host_config.base_url, ) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 50de11d8209a0..6a9b00d204174 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,8 +3,9 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["sonarr==0.3.0"], + "requirements": ["aiopyarr==22.2.2"], "config_flow": true, "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiopyarr"] } diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 8911927d73202..c182bb2bbebde 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,11 +1,16 @@ """Support for Sonarr sensors.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta +from functools import wraps import logging -from typing import Any +from typing import Any, TypeVar -from sonarr import Sonarr, SonarrConnectionError, SonarrError +from aiopyarr import ArrConnectionException, ArrException, SystemStatus +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.sonarr_client import SonarrClient +from typing_extensions import Concatenate, ParamSpec from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -15,7 +20,14 @@ from homeassistant.helpers.typing import StateType import homeassistant.util.dt as dt_util -from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DATA_SONARR, DOMAIN +from .const import ( + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DATA_HOST_CONFIG, + DATA_SONARR, + DATA_SYSTEM_STATUS, + DOMAIN, +) from .entity import SonarrEntity _LOGGER = logging.getLogger(__name__) @@ -64,6 +76,9 @@ ), ) +_T = TypeVar("_T", bound="SonarrSensor") +_P = ParamSpec("_P") + async def async_setup_entry( hass: HomeAssistant, @@ -71,46 +86,67 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Sonarr sensors based on a config entry.""" - sonarr: Sonarr = hass.data[DOMAIN][entry.entry_id][DATA_SONARR] + sonarr: SonarrClient = hass.data[DOMAIN][entry.entry_id][DATA_SONARR] + host_config: PyArrHostConfiguration = hass.data[DOMAIN][entry.entry_id][ + DATA_HOST_CONFIG + ] + system_status: SystemStatus = hass.data[DOMAIN][entry.entry_id][DATA_SYSTEM_STATUS] options: dict[str, Any] = dict(entry.options) entities = [ - SonarrSensor(sonarr, entry.entry_id, description, options) + SonarrSensor( + sonarr, + host_config, + system_status, + entry.entry_id, + description, + options, + ) for description in SENSOR_TYPES ] async_add_entities(entities, True) -def sonarr_exception_handler(func): +def sonarr_exception_handler( + func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] """Decorate Sonarr calls to handle Sonarr exceptions. A decorator that wraps the passed in function, catches Sonarr errors, and handles the availability of the entity. """ - async def handler(self, *args, **kwargs): + @wraps(func) + async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: try: await func(self, *args, **kwargs) self.last_update_success = True - except SonarrConnectionError as error: - if self.available: + except ArrConnectionException as error: + if self.last_update_success: _LOGGER.error("Error communicating with API: %s", error) self.last_update_success = False - except SonarrError as error: - if self.available: + except ArrException as error: + if self.last_update_success: _LOGGER.error("Invalid response from API: %s", error) self.last_update_success = False - return handler + return wrapper class SonarrSensor(SonarrEntity, SensorEntity): """Implementation of the Sonarr sensor.""" + data: dict[str, Any] + last_update_success: bool + upcoming_days: int + wanted_max_items: int + def __init__( self, - sonarr: Sonarr, + sonarr: SonarrClient, + host_config: PyArrHostConfiguration, + system_status: SystemStatus, entry_id: str, description: SensorEntityDescription, options: dict[str, Any], @@ -119,13 +155,15 @@ def __init__( self.entity_description = description self._attr_unique_id = f"{entry_id}_{description.key}" - self.data: dict[str, Any] = {} - self.last_update_success: bool = False - self.upcoming_days: int = options[CONF_UPCOMING_DAYS] - self.wanted_max_items: int = options[CONF_WANTED_MAX_ITEMS] + self.data = {} + self.last_update_success = True + self.upcoming_days = options[CONF_UPCOMING_DAYS] + self.wanted_max_items = options[CONF_WANTED_MAX_ITEMS] super().__init__( sonarr=sonarr, + host_config=host_config, + system_status=system_status, entry_id=entry_id, device_id=entry_id, ) @@ -141,23 +179,30 @@ async def async_update(self) -> None: key = self.entity_description.key if key == "diskspace": - await self.sonarr.update() + self.data[key] = await self.sonarr.async_get_diskspace() elif key == "commands": - self.data[key] = await self.sonarr.commands() + self.data[key] = await self.sonarr.async_get_commands() elif key == "queue": - self.data[key] = await self.sonarr.queue() + self.data[key] = await self.sonarr.async_get_queue( + include_series=True, include_episode=True + ) elif key == "series": - self.data[key] = await self.sonarr.series() + self.data[key] = await self.sonarr.async_get_series() elif key == "upcoming": local = dt_util.start_of_local_day().replace(microsecond=0) start = dt_util.as_utc(local) end = start + timedelta(days=self.upcoming_days) - self.data[key] = await self.sonarr.calendar( - start=start.isoformat(), end=end.isoformat() + self.data[key] = await self.sonarr.async_get_calendar( + start_date=start, + end_date=end, + include_series=True, ) elif key == "wanted": - self.data[key] = await self.sonarr.wanted(page_size=self.wanted_max_items) + self.data[key] = await self.sonarr.async_get_wanted( + page_size=self.wanted_max_items, + include_series=True, + ) @property def extra_state_attributes(self) -> dict[str, str] | None: @@ -165,10 +210,10 @@ def extra_state_attributes(self) -> dict[str, str] | None: attrs = {} key = self.entity_description.key - if key == "diskspace": - for disk in self.sonarr.app.disks: - free = disk.free / 1024 ** 3 - total = disk.total / 1024 ** 3 + if key == "diskspace" and self.data.get(key) is not None: + for disk in self.data[key]: + free = disk.freeSpace / 1024**3 + total = disk.totalSpace / 1024**3 usage = free / total * 100 attrs[ @@ -176,23 +221,33 @@ def extra_state_attributes(self) -> dict[str, str] | None: ] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)" elif key == "commands" and self.data.get(key) is not None: for command in self.data[key]: - attrs[command.name] = command.state + attrs[command.name] = command.status elif key == "queue" and self.data.get(key) is not None: - for item in self.data[key]: - remaining = 1 if item.size == 0 else item.size_remaining / item.size + for item in self.data[key].records: + remaining = 1 if item.size == 0 else item.sizeleft / item.size remaining_pct = 100 * (1 - remaining) - name = f"{item.episode.series.title} {item.episode.identifier}" + identifier = f"S{item.episode.seasonNumber:02d}E{item.episode. episodeNumber:02d}" + + name = f"{item.series.title} {identifier}" attrs[name] = f"{remaining_pct:.2f}%" elif key == "series" and self.data.get(key) is not None: for item in self.data[key]: - attrs[item.series.title] = f"{item.downloaded}/{item.episodes} Episodes" + stats = item.statistics + attrs[ + item.title + ] = f"{stats.episodeFileCount}/{stats.episodeCount} Episodes" elif key == "upcoming" and self.data.get(key) is not None: for episode in self.data[key]: - attrs[episode.series.title] = episode.identifier + identifier = f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}" + attrs[episode.series.title] = identifier elif key == "wanted" and self.data.get(key) is not None: - for episode in self.data[key].episodes: - name = f"{episode.series.title} {episode.identifier}" - attrs[name] = episode.airdate + for item in self.data[key].records: + identifier = f"S{item.seasonNumber:02d}E{item.episodeNumber:02d}" + + name = f"{item.series.title} {identifier}" + attrs[name] = dt_util.as_local( + item.airDateUtc.replace(tzinfo=dt_util.UTC) + ).isoformat() return attrs @@ -201,16 +256,16 @@ def native_value(self) -> StateType: """Return the state of the sensor.""" key = self.entity_description.key - if key == "diskspace": - total_free = sum(disk.free for disk in self.sonarr.app.disks) - free = total_free / 1024 ** 3 + if key == "diskspace" and self.data.get(key) is not None: + total_free = sum(disk.freeSpace for disk in self.data[key]) + free = total_free / 1024**3 return f"{free:.2f}" if key == "commands" and self.data.get(key) is not None: return len(self.data[key]) if key == "queue" and self.data.get(key) is not None: - return len(self.data[key]) + return self.data[key].totalRecords if key == "series" and self.data.get(key) is not None: return len(self.data[key]) @@ -219,6 +274,6 @@ def native_value(self) -> StateType: return len(self.data[key]) if key == "wanted" and self.data.get(key) is not None: - return self.data[key].total + return self.data[key].totalRecords return None diff --git a/homeassistant/components/sonarr/strings.json b/homeassistant/components/sonarr/strings.json index 2281b6cec57a4..b8537e11442c7 100644 --- a/homeassistant/components/sonarr/strings.json +++ b/homeassistant/components/sonarr/strings.json @@ -4,17 +4,14 @@ "step": { "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", + "url": "[%key:common::config_flow::data::url%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "base_path": "Path to API", - "port": "[%key:common::config_flow::data::port%]", - "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}" + "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {url}" } }, "error": { diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json index 10930df852538..d3cb57208752e 100644 --- a/homeassistant/components/sonarr/translations/ca.json +++ b/homeassistant/components/sonarr/translations/ca.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "La integraci\u00f3 de Sonarr ha de tornar a autenticar-se manualment amb l'API de Sonarr allotjada a: {host}", + "description": "La integraci\u00f3 de Sonarr ha de tornar a autenticar-se manualment amb l'API de Sonarr allotjada a: {url}", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { @@ -22,6 +22,7 @@ "host": "Amfitri\u00f3", "port": "Port", "ssl": "Utilitza un certificat SSL", + "url": "URL", "verify_ssl": "Verifica el certificat SSL" } } diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index 9779a9850344a..eb521c1c2375f 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Die Sonarr-Integration muss manuell mit der Sonarr-API, die unter {host} gehostet wird, neu authentifiziert werden", + "description": "Die Sonarr-Integration muss manuell erneut mit der Sonarr-API authentifiziert werden, die unter folgender Adresse gehostet wird: {url}", "title": "Integration erneut authentifizieren" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", + "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json index cdaa3e16590a8..a348d76f79895 100644 --- a/homeassistant/components/sonarr/translations/el.json +++ b/homeassistant/components/sonarr/translations/el.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1" + "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": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "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", + "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" }, "flow_title": "{name}", "step": { @@ -11,7 +17,13 @@ }, "user": { "data": { - "base_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf API" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "base_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\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", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "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" } } } diff --git a/homeassistant/components/sonarr/translations/en.json b/homeassistant/components/sonarr/translations/en.json index 74232e9f8b793..f676005dcfe42 100644 --- a/homeassistant/components/sonarr/translations/en.json +++ b/homeassistant/components/sonarr/translations/en.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}", + "description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {url}", "title": "Reauthenticate Integration" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Port", "ssl": "Uses an SSL certificate", + "url": "URL", "verify_ssl": "Verify SSL certificate" } } diff --git a/homeassistant/components/sonarr/translations/pt-BR.json b/homeassistant/components/sonarr/translations/pt-BR.json new file mode 100644 index 0000000000000..4c474ef2349d1 --- /dev/null +++ b/homeassistant/components/sonarr/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Sonarr precisa ser autenticada manualmente novamente com a API do Sonarr hospedada em: {url}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave da API", + "base_path": "Caminho para a API", + "host": "Nome do host", + "port": "Porta", + "ssl": "Usar um certificado SSL", + "url": "URL", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos", + "wanted_max_items": "N\u00famero m\u00e1ximo de itens desejados para exibir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/sk.json b/homeassistant/components/sonarr/translations/sk.json new file mode 100644 index 0000000000000..6e1cd075aa932 --- /dev/null +++ b/homeassistant/components/sonarr/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 4d417aec1a2f0..8825de877e500 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.12"], + "requirements": ["python-songpal==0.14.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { @@ -12,5 +12,6 @@ } ], "quality_scale": "gold", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["songpal"] } diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 7492bd536e364..e161f818c8c05 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -212,13 +212,18 @@ def name(self): @property def unique_id(self): """Return a unique ID.""" - return self._sysinfo.macAddr + return self._sysinfo.macAddr or self._sysinfo.wirelessMacAddr @property def device_info(self) -> DeviceInfo: """Return the device info.""" + connections = set() + if self._sysinfo.macAddr: + connections.add((dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)) + if self._sysinfo.wirelessMacAddr: + connections.add((dr.CONNECTION_NETWORK_MAC, self._sysinfo.wirelessMacAddr)) return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)}, + connections=connections, identifiers={(DOMAIN, self.unique_id)}, manufacturer="Sony Corporation", model=self._model, diff --git a/homeassistant/components/songpal/translations/el.json b/homeassistant/components/songpal/translations/el.json index 59827459f94c6..6e078b24305ad 100644 --- a/homeassistant/components/songpal/translations/el.json +++ b/homeassistant/components/songpal/translations/el.json @@ -1,7 +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", "not_songpal_device": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Songpal" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{name} ({host})", + "step": { + "init": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" + }, + "user": { + "data": { + "endpoint": "\u03a4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/pt-BR.json b/homeassistant/components/songpal/translations/pt-BR.json index 110e74131217c..0a6fcbb52c2df 100644 --- a/homeassistant/components/songpal/translations/pt-BR.json +++ b/homeassistant/components/songpal/translations/pt-BR.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "not_songpal_device": "N\u00e3o \u00e9 um dispositivo Songpal" + }, "error": { "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name} ({host})", + "step": { + "init": { + "description": "Deseja configurar {name} ( {host} )?" + }, + "user": { + "data": { + "endpoint": "Ponto final" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 673e0ac4dfe46..27d51d8f3e63a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -38,6 +38,7 @@ SONOS_CHECK_ACTIVITY, SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, + SONOS_VANISHED, UPNP_ST, ) from .favorites import SonosFavorites @@ -118,6 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" soco_config.EVENTS_MODULE = events_asyncio + soco_config.REQUEST_TIMEOUT = 9.5 if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -180,6 +182,9 @@ def _create_soco(self, ip_address: str, source: SoCoCreationSource) -> SoCo | No soco = SoCo(ip_address) # Ensure that the player is available and UID is cached uid = soco.uid + # Abort early if the device is not visible + if not soco.is_visible: + return None _ = soco.volume return soco except NotSupportedException as exc: @@ -238,8 +243,7 @@ def _manual_hosts(self, now: datetime.datetime | None = None) -> None: None, ) if not known_uid: - soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED): self._discovered_player(soco) self.data.hosts_heartbeat = call_later( @@ -247,8 +251,7 @@ def _manual_hosts(self, now: datetime.datetime | None = None) -> None: ) def _discovered_ip(self, ip_address): - soco = self._create_soco(ip_address, SoCoCreationSource.DISCOVERED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_address, SoCoCreationSource.DISCOVERED): self._discovered_player(soco) async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum): @@ -274,14 +277,19 @@ async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum) async def _async_ssdp_discovered_player( self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange ) -> None: - if change == ssdp.SsdpChange.BYEBYE: - return - uid = info.upnp[ssdp.ATTR_UPNP_UDN] if not uid.startswith("uuid:RINCON_"): return - uid = uid[5:] + + if change == ssdp.SsdpChange.BYEBYE: + _LOGGER.debug( + "ssdp:byebye received from %s", info.upnp.get("friendlyName", uid) + ) + reason = info.ssdp_headers.get("X-RINCON-REASON", "ssdp:byebye") + async_dispatcher_send(self.hass, f"{SONOS_VANISHED}-{uid}", reason) + return + discovered_ip = urlparse(info.ssdp_location).hostname boot_seqnum = info.ssdp_headers.get("X-RINCON-BOOTSEQ") self.async_discovered_player( @@ -336,7 +344,6 @@ async def setup_platforms_and_discovery(self): ) ) await self.hass.async_add_executor_job(self._manual_hosts) - return self.entry.async_on_unload( await ssdp.async_register_callback( diff --git a/homeassistant/components/sonos/alarms.py b/homeassistant/components/sonos/alarms.py index dfe9e8328a45e..e7cf05a1ff025 100644 --- a/homeassistant/components/sonos/alarms.py +++ b/homeassistant/components/sonos/alarms.py @@ -8,11 +8,11 @@ from soco import SoCo from soco.alarms import Alarm, Alarms from soco.events_base import Event as SonosEvent -from soco.exceptions import SoCoException from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM +from .helpers import soco_error from .household_coordinator import SonosHouseholdCoordinator if TYPE_CHECKING: @@ -71,13 +71,10 @@ async def async_process_event( speaker.event_stats.process(event) await self.async_update_entities(speaker.soco, event_id) + @soco_error() def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: """Update cache of known alarms and return if cache has changed.""" - try: - self.alarms.update(soco) - except (OSError, SoCoException) as err: - _LOGGER.error("Could not update alarms using %s: %s", soco, err) - return False + self.alarms.update(soco) if update_id and self.alarms.last_id < update_id: # Skip updates if latest query result is outdated or lagging diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 008e0b2cf5751..4eaa75f92ae34 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -16,6 +16,7 @@ from .const import SONOS_CREATE_BATTERY, SONOS_CREATE_MIC_SENSOR from .entity import SonosEntity +from .helpers import soco_error from .speaker import SonosSpeaker ATTR_BATTERY_POWER_SOURCE = "power_source" @@ -64,7 +65,7 @@ def __init__(self, speaker: SonosSpeaker) -> None: self._attr_unique_id = f"{self.soco.uid}-power" self._attr_name = f"{self.speaker.zone_name} Power" - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() @@ -98,8 +99,14 @@ def __init__(self, speaker: SonosSpeaker) -> None: self._attr_unique_id = f"{self.soco.uid}-microphone" self._attr_name = f"{self.speaker.zone_name} Microphone" - async def _async_poll(self) -> None: - """Stub for abstract class implementation. Not a pollable attribute.""" + async def _async_fallback_poll(self) -> None: + """Handle polling when subscription fails.""" + await self.hass.async_add_executor_job(self.poll_state) + + @soco_error() + def poll_state(self) -> None: + """Poll the current state of the microphone.""" + self.speaker.mic_enabled = self.soco.mic_enabled @property def is_on(self) -> bool: diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 07add8e6d7c78..cc453f146911b 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,7 +1,7 @@ """Config flow for SONOS.""" +from collections.abc import Awaitable import dataclasses -from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -16,7 +16,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: return bool(await ssdp.async_get_discovery_info_by_st(hass, UPNP_ST)) -class SonosDiscoveryFlowHandler(DiscoveryFlowHandler): +class SonosDiscoveryFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): """Sonos discovery flow that callsback zeroconf updates.""" def __init__(self) -> None: @@ -42,6 +42,3 @@ async def async_step_zeroconf( "Zeroconf", properties, host, uid, boot_seqnum, model, mdns_name ) return await self.async_step_discovery(dataclasses.asdict(discovery_info)) - - -config_entries.HANDLERS.register(DOMAIN)(SonosDiscoveryFlowHandler) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 6aec401b4129a..6c4bdd07b3141 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -156,16 +156,19 @@ SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" -SONOS_POLL_UPDATE = "sonos_poll_update" +SONOS_FALLBACK_POLL = "sonos_fallback_poll" SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" +SONOS_MEDIA_UPDATED = "sonos_media_updated" SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity" SONOS_SPEAKER_ADDED = "sonos_speaker_added" SONOS_STATE_UPDATED = "sonos_state_updated" SONOS_REBOOTED = "sonos_rebooted" SONOS_VANISHED = "sonos_vanished" +SOURCE_AIRPLAY = "AirPlay" SOURCE_LINEIN = "Line-in" +SOURCE_SPOTIFY_CONNECT = "Spotify Connect" SOURCE_TV = "TV" AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 53768431a1d1d..8565fe08e9c9e 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -7,7 +7,6 @@ import soco.config as soco_config from soco.core import SoCo -from soco.exceptions import SoCoException from homeassistant.components import persistent_notification import homeassistant.helpers.device_registry as dr @@ -17,10 +16,11 @@ from .const import ( DATA_SONOS, DOMAIN, + SONOS_FALLBACK_POLL, SONOS_FAVORITES_UPDATED, - SONOS_POLL_UPDATE, SONOS_STATE_UPDATED, ) +from .exception import SonosUpdateError from .speaker import SonosSpeaker SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements" @@ -43,8 +43,8 @@ async def async_added_to_hass(self) -> None: self.async_on_remove( async_dispatcher_connect( self.hass, - f"{SONOS_POLL_UPDATE}-{self.soco.uid}", - self.async_poll, + f"{SONOS_FALLBACK_POLL}-{self.soco.uid}", + self.async_fallback_poll, ) ) self.async_on_remove( @@ -66,7 +66,7 @@ async def async_will_remove_from_hass(self) -> None: """Clean up when entity is removed.""" del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] - async def async_poll(self, now: datetime.datetime) -> None: + async def async_fallback_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" if not self.speaker.subscriptions_failed: if soco_config.EVENT_ADVERTISE_IP: @@ -86,13 +86,16 @@ async def async_poll(self, now: datetime.datetime) -> None: self.speaker.subscriptions_failed = True await self.speaker.async_unsubscribe() try: - await self._async_poll() - except (OSError, SoCoException) as ex: - _LOGGER.debug("Error connecting to %s: %s", self.entity_id, ex) + await self._async_fallback_poll() + except SonosUpdateError as err: + _LOGGER.debug("Could not fallback poll: %s", err) @abstractmethod - async def _async_poll(self) -> None: - """Poll the specific functionality. Should be implemented by platforms if needed.""" + async def _async_fallback_poll(self) -> None: + """Poll the specific functionality if subscriptions fail. + + Should be implemented by platforms if needed. + """ @property def soco(self) -> SoCo: @@ -120,3 +123,20 @@ def device_info(self) -> DeviceInfo: def available(self) -> bool: """Return whether this device is available.""" return self.speaker.available + + +class SonosPollingEntity(SonosEntity): + """Representation of a Sonos entity which may not support updating by subscriptions.""" + + @abstractmethod + def poll_state(self) -> None: + """Poll the device for the current state.""" + + def update(self) -> None: + """Update the state using the built-in entity poller.""" + if not self.available: + return + try: + self.poll_state() + except SonosUpdateError as err: + _LOGGER.debug("Could not poll: %s", err) diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index d7f1a2e6a9630..bce1e3233c128 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -7,5 +7,5 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" -class SpeakerUnavailable(HomeAssistantError): - """Speaker is unavailable.""" +class SonosUpdateError(HomeAssistantError): + """Update failed.""" diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 64c5b7438099a..fd651b7740ccc 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -14,6 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import SONOS_FAVORITES_UPDATED +from .helpers import soco_error from .household_coordinator import SonosHouseholdCoordinator if TYPE_CHECKING: @@ -90,6 +91,7 @@ async def async_process_event( self.last_processed_event_id = event_id await self.async_update_entities(speaker.soco, container_id) + @soco_error() def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: """Update cache of known favorites and return if cache has changed.""" new_favorites = soco.music_library.get_sonos_favorites() diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 6da25314f0f50..a11847d2b0ca7 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,19 +3,20 @@ from collections.abc import Callable import logging -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar +from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException from typing_extensions import Concatenate, ParamSpec -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from .const import SONOS_SPEAKER_ACTIVITY -from .exception import SpeakerUnavailable +from .exception import SonosUpdateError if TYPE_CHECKING: from .entity import SonosEntity + from .household_coordinator import SonosHouseholdCoordinator from .speaker import SonosSpeaker UID_PREFIX = "RINCON_" @@ -23,13 +24,13 @@ _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T", bound="SonosSpeaker | SonosEntity") +_T = TypeVar("_T", bound="SonosSpeaker | SonosEntity | SonosHouseholdCoordinator") _R = TypeVar("_R") _P = ParamSpec("_P") def soco_error( - errorcodes: list[str] | None = None, raise_on_err: bool = True + errorcodes: list[str] | None = None, ) -> Callable[ # type: ignore[misc] [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R | None] ]: @@ -42,10 +43,9 @@ def decorator( def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: """Wrap for all soco UPnP exception.""" + args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) try: result = funct(self, *args, **kwargs) - except SpeakerUnavailable: - return None except (OSError, SoCoException, SoCoUPnPException) as err: error_code = getattr(err, "error_code", None) function = funct.__qualname__ @@ -55,20 +55,16 @@ def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: ) return None - # Prefer the entity_id if available, zone name as a fallback - # Needed as SonosSpeaker instances are not entities - zone_name = getattr(self, "speaker", self).zone_name - target = getattr(self, "entity_id", zone_name) - message = f"Error calling {function} on {target}: {err}" - if raise_on_err: - raise HomeAssistantError(message) from err + if (target := _find_target_identifier(self, args_soco)) is None: + raise RuntimeError("Unexpected use of soco_error") from err - _LOGGER.warning(message) - return None + message = f"Error calling {function} on {target}: {err}" + raise SonosUpdateError(message) from err + dispatch_soco = args_soco or self.soco dispatcher_send( self.hass, - f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", + f"{SONOS_SPEAKER_ACTIVITY}-{dispatch_soco.uid}", funct.__qualname__, ) return result @@ -78,6 +74,24 @@ def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: return decorator +def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None: + """Extract the the best available target identifier from the provided instance object.""" + if entity_id := getattr(instance, "entity_id", None): + # SonosEntity instance + return entity_id + if zone_name := getattr(instance, "zone_name", None): + # SonosSpeaker instance + return zone_name + if speaker := getattr(instance, "speaker", None): + # Holds a SonosSpeaker instance attribute + return speaker.zone_name + if soco := getattr(instance, "soco", fallback_soco): + # Holds a SoCo instance attribute + # Only use attributes with no I/O + return soco._player_name or soco.ip_address # pylint: disable=protected-access + return None + + def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" if hostname.startswith("Sonos-"): diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index f233b33827926..0d76feae461e9 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -6,12 +6,12 @@ import logging from soco import SoCo -from soco.exceptions import SoCoException from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from .const import DATA_SONOS +from .exception import SonosUpdateError _LOGGER = logging.getLogger(__name__) @@ -56,11 +56,10 @@ async def _async_poll(self) -> None: _LOGGER.debug("Polling %s using %s", self.class_type, speaker.soco) try: await self.async_update_entities(speaker.soco) - except (OSError, SoCoException) as err: + except SonosUpdateError as err: _LOGGER.error( - "Could not refresh %s using %s: %s", + "Could not refresh %s: %s", self.class_type, - speaker.soco, err, ) else: diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index bd16701435cba..4bb8623acb23d 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.26.2"], + "requirements": ["soco==0.26.3"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], @@ -13,5 +13,6 @@ } ], "codeowners": ["@cgtobi", "@jjlawren"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["soco"] } diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py new file mode 100644 index 0000000000000..f4108b8531763 --- /dev/null +++ b/homeassistant/components/sonos/media.py @@ -0,0 +1,235 @@ +"""Support for media metadata handling.""" +from __future__ import annotations + +import datetime +import logging +from typing import Any + +from soco.core import ( + MUSIC_SRC_AIRPLAY, + MUSIC_SRC_LINE_IN, + MUSIC_SRC_RADIO, + MUSIC_SRC_SPOTIFY_CONNECT, + MUSIC_SRC_TV, + SoCo, +) +from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer +from soco.music_library import MusicLibrary + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import time_period_str +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.util import dt as dt_util + +from .const import ( + SONOS_MEDIA_UPDATED, + SONOS_STATE_PLAYING, + SONOS_STATE_TRANSITIONING, + SOURCE_AIRPLAY, + SOURCE_LINEIN, + SOURCE_SPOTIFY_CONNECT, + SOURCE_TV, +) +from .helpers import soco_error + +LINEIN_SOURCES = (MUSIC_SRC_TV, MUSIC_SRC_LINE_IN) +SOURCE_MAPPING = { + MUSIC_SRC_AIRPLAY: SOURCE_AIRPLAY, + MUSIC_SRC_TV: SOURCE_TV, + MUSIC_SRC_LINE_IN: SOURCE_LINEIN, + MUSIC_SRC_SPOTIFY_CONNECT: SOURCE_SPOTIFY_CONNECT, +} +UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} +DURATION_SECONDS = "duration_in_s" +POSITION_SECONDS = "position_in_s" + +_LOGGER = logging.getLogger(__name__) + + +def _timespan_secs(timespan: str | None) -> None | float: + """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] + + +class SonosMedia: + """Representation of the current Sonos media.""" + + def __init__(self, hass: HomeAssistant, soco: SoCo) -> None: + """Initialize a SonosMedia.""" + self.hass = hass + self.soco = soco + self.play_mode: str | None = None + self.playback_status: str | None = None + + # This block is reset with clear() + self.album_name: str | None = None + self.artist: str | None = None + self.channel: str | None = None + self.duration: float | None = None + self.image_url: str | None = None + self.queue_position: int | None = None + self.queue_size: int | None = None + self.playlist_name: str | None = None + self.source_name: str | None = None + self.title: str | None = None + self.uri: str | None = None + + self.position: float | None = None + self.position_updated_at: datetime.datetime | None = None + + def clear(self) -> None: + """Clear basic media info.""" + self.album_name = None + self.artist = None + self.channel = None + self.duration = None + self.image_url = None + self.playlist_name = None + self.queue_position = None + self.queue_size = None + self.source_name = None + self.title = None + self.uri = None + + def clear_position(self) -> None: + """Clear the position attributes.""" + self.position = None + self.position_updated_at = None + + @property + def library(self) -> MusicLibrary: + """Return the soco MusicLibrary instance.""" + return self.soco.music_library + + @soco_error() + def poll_track_info(self) -> dict[str, Any]: + """Poll the speaker for current track info, add converted position values, and return.""" + track_info = self.soco.get_current_track_info() + track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) + track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) + return track_info + + def write_media_player_states(self) -> None: + """Send a signal to media player(s) to write new states.""" + dispatcher_send(self.hass, SONOS_MEDIA_UPDATED, self.soco.uid) + + def set_basic_track_info(self, update_position: bool = False) -> None: + """Query the speaker to update media metadata and position info.""" + self.clear() + + track_info = self.poll_track_info() + self.uri = track_info["uri"] + + audio_source = self.soco.music_source_from_uri(self.uri) + if source := SOURCE_MAPPING.get(audio_source): + self.source_name = source + if audio_source in LINEIN_SOURCES: + self.clear_position() + self.title = source + return + + self.artist = track_info.get("artist") + self.album_name = track_info.get("album") + self.title = track_info.get("title") + self.image_url = track_info.get("album_art") + + playlist_position = int(track_info.get("playlist_position")) + if playlist_position > 0: + self.queue_position = playlist_position + + self.update_media_position(track_info, force_update=update_position) + + def update_media_from_event(self, evars: dict[str, Any]) -> None: + """Update information about currently playing media using an event payload.""" + new_status = evars["transport_state"] + state_changed = new_status != self.playback_status + + self.play_mode = evars["current_play_mode"] + self.playback_status = new_status + + track_uri = evars["enqueued_transport_uri"] or evars["current_track_uri"] + audio_source = self.soco.music_source_from_uri(track_uri) + + self.set_basic_track_info(update_position=state_changed) + + if ct_md := evars["current_track_meta_data"]: + if not self.image_url: + if album_art_uri := getattr(ct_md, "album_art_uri", None): + self.image_url = self.library.build_album_art_full_uri( + album_art_uri + ) + + et_uri_md = evars["enqueued_transport_uri_meta_data"] + if isinstance(et_uri_md, DidlPlaylistContainer): + self.playlist_name = et_uri_md.title + + if queue_size := evars.get("number_of_tracks", 0): + self.queue_size = int(queue_size) + + if audio_source == MUSIC_SRC_RADIO: + if et_uri_md: + self.channel = et_uri_md.title + + if ct_md and ct_md.radio_show: + radio_show = ct_md.radio_show.split(",")[0] + self.channel = " • ".join(filter(None, [self.channel, radio_show])) + + if isinstance(et_uri_md, DidlAudioBroadcast): + self.title = self.title or self.channel + + self.write_media_player_states() + + @soco_error() + def poll_media(self) -> None: + """Poll information about currently playing media.""" + transport_info = self.soco.get_current_transport_info() + new_status = transport_info["current_transport_state"] + + if new_status == SONOS_STATE_TRANSITIONING: + return + + update_position = new_status != self.playback_status + self.playback_status = new_status + self.play_mode = self.soco.play_mode + + self.set_basic_track_info(update_position=update_position) + + self.write_media_player_states() + + def update_media_position( + self, position_info: dict[str, int], force_update: bool = False + ) -> None: + """Update state when playing music tracks.""" + if (duration := position_info.get(DURATION_SECONDS)) == 0: + self.clear_position() + return + + should_update = force_update + self.duration = duration + current_position = position_info.get(POSITION_SECONDS) + + # player started reporting position? + if current_position is not None and self.position is None: + should_update = True + + # position jumped? + if current_position is not None and self.position is not None: + if self.playback_status == SONOS_STATE_PLAYING: + assert self.position_updated_at is not None + time_delta = dt_util.utcnow() - self.position_updated_at + time_diff = time_delta.total_seconds() + else: + time_diff = 0 + + calculated_position = self.position + time_diff + + if abs(calculated_position - current_position) > 1.5: + should_update = True + + if current_position is None: + self.clear_position() + elif should_update: + self.position = current_position + self.position_updated_at = dt_util.utcnow() diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 2d9714699284c..b2d881e8bf279 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -105,9 +105,6 @@ async def async_browse_media( hass, media_content_type, media_content_id, can_play_artist=False ) - if media_content_type == "spotify": - return await spotify.async_browse_media(hass, None, None, can_play_artist=False) - if media_content_type == "library": return await hass.async_add_executor_job( library_payload, @@ -270,6 +267,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="favorites", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) @@ -284,6 +282,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="library", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) @@ -303,17 +302,8 @@ async def root_payload( ) if "spotify" in hass.config.components: - children.append( - BrowseMedia( - title="Spotify", - media_class=MEDIA_CLASS_APP, - media_content_id="", - media_content_type="spotify", - thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", - can_play=False, - can_expand=True, - ) - ) + result = await spotify.async_browse_media(hass, None, None) + children.extend(result.children) try: item = await media_source.async_browse_media( diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 41453117c13ea..65e8c4111ae04 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -6,7 +6,6 @@ import json import logging from typing import Any -from urllib.parse import quote from soco import alarms from soco.core import ( @@ -19,9 +18,12 @@ import voluptuous as vol from homeassistant.components import media_source, spotify -from homeassistant.components.http.auth import async_sign_path -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import ( + MediaPlayerEntity, + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -56,7 +58,6 @@ from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.network import get_url from . import media_browser from .const import ( @@ -65,6 +66,7 @@ MEDIA_TYPES_TO_SONOS, PLAYABLE_MEDIA_TYPES, SONOS_CREATE_MEDIA_PLAYER, + SONOS_MEDIA_UPDATED, SONOS_STATE_PLAYING, SONOS_STATE_TRANSITIONING, SOURCE_LINEIN, @@ -255,6 +257,23 @@ def __init__(self, speaker: SonosSpeaker) -> None: self._attr_unique_id = self.soco.uid self._attr_name = self.speaker.zone_name + async def async_added_to_hass(self) -> None: + """Handle common setup when added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SONOS_MEDIA_UPDATED, + self.async_write_media_state, + ) + ) + + @callback + def async_write_media_state(self, uid: str) -> None: + """Write media state if the provided UID is coordinator of this speaker.""" + if self.coordinator.uid == uid: + self.async_write_ha_state() + @property def coordinator(self) -> SonosSpeaker: """Return the current coordinator SonosSpeaker.""" @@ -283,7 +302,7 @@ def state(self) -> str: return STATE_PLAYING return STATE_IDLE - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Retrieve latest state by polling.""" await self.hass.data[DATA_SONOS].favorites[ self.speaker.household_id @@ -295,7 +314,7 @@ def _update(self) -> None: self.speaker.update_groups() self.speaker.update_volume() if self.speaker.is_coordinator: - self.speaker.update_media() + self.media.poll_media() @property def volume_level(self) -> float | None: @@ -522,8 +541,12 @@ def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """ if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) + media_id = spotify.spotify_uri_from_media_browser_url(media_id) + + is_radio = False if media_source.is_media_source_id(media_id): + is_radio = media_id.startswith("media-source://radio_browser/") media_type = MEDIA_TYPE_MUSIC media_id = ( run_coroutine_threadsafe( @@ -568,22 +591,12 @@ def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: soco.play_from_queue(0) elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): # If media ID is a relative URL, we serve it from HA. - # Create a signed path. - if media_id[0] == "/": - media_id = async_sign_path( - self.hass, - quote(media_id), - datetime.timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), - ) - - # prepend external URL - hass_url = get_url(self.hass, prefer_external=True) - media_id = f"{hass_url}{media_id}" + media_id = async_process_play_media_url(self.hass, media_id) if kwargs.get(ATTR_MEDIA_ENQUEUE): soco.add_uri_to_queue(media_id) else: - soco.play_uri(media_id) + soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] @@ -666,6 +679,12 @@ def extra_state_attributes(self) -> dict[str, Any]: if self.media.queue_position is not None: attributes[ATTR_QUEUE_POSITION] = self.media.queue_position + if self.media.queue_size: + attributes["queue_size"] = self.media.queue_size + + if self.source: + attributes[ATTR_INPUT_SOURCE] = self.source + return attributes async def async_get_browse_image( diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 2e6acd55a6678..c9b8ec4758339 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -12,7 +12,6 @@ from .const import SONOS_CREATE_LEVELS from .entity import SonosEntity -from .exception import SpeakerUnavailable from .helpers import soco_error from .speaker import SonosSpeaker @@ -75,16 +74,13 @@ def __init__( self.level_type = level_type self._attr_min_value, self._attr_max_value = valid_range - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Poll the value if subscriptions are not working.""" - await self.hass.async_add_executor_job(self.update) - - @soco_error(raise_on_err=False) - def update(self) -> None: - """Fetch number state if necessary.""" - if not self.available: - raise SpeakerUnavailable + await self.hass.async_add_executor_job(self.poll_state) + @soco_error() + def poll_state(self) -> None: + """Poll the device for the current state.""" state = getattr(self.soco, self.level_type) setattr(self.speaker, self.level_type, state) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 4e86edc2ca3b4..f011cb2d7540a 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -11,8 +11,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY -from .entity import SonosEntity +from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY, SOURCE_TV +from .entity import SonosEntity, SonosPollingEntity +from .helpers import soco_error from .speaker import SonosSpeaker _LOGGER = logging.getLogger(__name__) @@ -64,7 +65,7 @@ def __init__(self, speaker: SonosSpeaker) -> None: self._attr_unique_id = f"{self.soco.uid}-battery" self._attr_name = f"{self.speaker.zone_name} Battery" - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() @@ -79,7 +80,7 @@ def available(self) -> bool: return self.speaker.available and self.speaker.power_source -class SonosAudioInputFormatSensorEntity(SonosEntity, SensorEntity): +class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): """Representation of a Sonos audio import format sensor entity.""" _attr_entity_category = EntityCategory.DIAGNOSTIC @@ -93,9 +94,16 @@ def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_native_value = audio_format - def update(self) -> None: + def poll_state(self) -> None: + """Poll the state if TV source is active and state has settled.""" + if self.speaker.media.source_name != SOURCE_TV and self.state == "No input": + return + self._poll_state() + + @soco_error() + def _poll_state(self) -> None: """Poll the device for the current state.""" self._attr_native_value = self.soco.soundbar_audio_input_format - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Provide a stub for required ABC method.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ca1e5a0a91ccf..3a2bac516841c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -9,15 +9,12 @@ import logging import time from typing import Any -import urllib.parse import async_timeout import defusedxml.ElementTree as ET -from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo -from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer +from soco.core import SoCo from soco.events_base import Event as SonosEvent, SubscriptionBase -from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException -from soco.music_library import MusicLibrary +from soco.exceptions import SoCoException, SoCoUPnPException from soco.plugins.plex import PlexPlugin from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot @@ -50,7 +47,7 @@ SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_MIC_SENSOR, SONOS_CREATE_SWITCHES, - SONOS_POLL_UPDATE, + SONOS_FALLBACK_POLL, SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, SONOS_SPEAKER_ADDED, @@ -58,12 +55,11 @@ SONOS_STATE_TRANSITIONING, SONOS_STATE_UPDATED, SONOS_VANISHED, - SOURCE_LINEIN, - SOURCE_TV, SUBSCRIPTION_TIMEOUT, ) from .favorites import SonosFavorites from .helpers import soco_error +from .media import SonosMedia from .statistics import ActivityStatistics, EventStatistics NEVER_TIME = -1200.0 @@ -79,7 +75,7 @@ "renderingControl", "zoneGroupTopology", ] -UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} +SUPPORTED_VANISH_REASONS = ("sleeping", "upgrade") UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] @@ -96,57 +92,6 @@ def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: return soco.get_battery_info() -def _timespan_secs(timespan: str | None) -> None | float: - """Parse a time-span into number of seconds.""" - if timespan in UNAVAILABLE_VALUES: - return None - - assert timespan is not None - return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) - - -class SonosMedia: - """Representation of the current Sonos media.""" - - def __init__(self, soco: SoCo) -> None: - """Initialize a SonosMedia.""" - self.library = MusicLibrary(soco) - self.play_mode: str | None = None - self.playback_status: str | None = None - - self.album_name: str | None = None - self.artist: str | None = None - self.channel: str | None = None - self.duration: float | None = None - self.image_url: str | None = None - self.queue_position: int | None = None - self.playlist_name: str | None = None - self.source_name: str | None = None - self.title: str | None = None - self.uri: str | None = None - - self.position: float | None = None - self.position_updated_at: datetime.datetime | None = None - - def clear(self) -> None: - """Clear basic media info.""" - self.album_name = None - self.artist = None - self.channel = None - self.duration = None - self.image_url = None - self.playlist_name = None - self.queue_position = None - self.source_name = None - self.title = None - self.uri = None - - def clear_position(self) -> None: - """Clear the position attributes.""" - self.position = None - self.position_updated_at = None - - class SonosSpeaker: """Representation of a Sonos speaker.""" @@ -157,7 +102,7 @@ def __init__( self.hass = hass self.soco = soco self.household_id: str = soco.household_id - self.media = SonosMedia(soco) + self.media = SonosMedia(hass, soco) self._plex_plugin: PlexPlugin | None = None self._share_link_plugin: ShareLinkPlugin | None = None self.available = True @@ -175,7 +120,7 @@ def __init__( # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] - self._resubscription_lock: asyncio.Lock | None = None + self._subscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME self._last_event_cache: dict[str, Any] = {} @@ -262,6 +207,10 @@ def setup(self, entry: ConfigEntry) -> None: ) dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) + if (mic_enabled := self.soco.mic_enabled) is not None: + self.mic_enabled = mic_enabled + dispatcher_send(self.hass, SONOS_CREATE_MIC_SENSOR, self) + if new_alarms := [ alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid ]: @@ -343,8 +292,41 @@ def subscription_address(self) -> str | None: # # Subscription handling and event dispatchers # - async def async_subscribe(self) -> bool: - """Initiate event subscriptions.""" + def log_subscription_result( + self, result: Any, event: str, level: str = logging.DEBUG + ) -> None: + """Log a message if a subscription action (create/renew/stop) results in an exception.""" + if not isinstance(result, Exception): + return + + if isinstance(result, asyncio.exceptions.TimeoutError): + message = "Request timed out" + exc_info = None + else: + message = result + exc_info = result if not str(result) else None + + _LOGGER.log( + level, + "%s failed for %s: %s", + event, + self.zone_name, + message, + exc_info=exc_info, + ) + + async def async_subscribe(self) -> None: + """Initiate event subscriptions under an async lock.""" + if not self._subscription_lock: + self._subscription_lock = asyncio.Lock() + + async with self._subscription_lock: + if self._subscriptions: + return + await self._async_subscribe() + + async def _async_subscribe(self) -> None: + """Create event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) # Create a polling task in case subscriptions fail or callback events do not arrive @@ -354,29 +336,20 @@ async def async_subscribe(self) -> bool: partial( async_dispatcher_send, self.hass, - f"{SONOS_POLL_UPDATE}-{self.soco.uid}", + f"{SONOS_FALLBACK_POLL}-{self.soco.uid}", ), SCAN_INTERVAL, ) - try: - await self.hass.async_add_executor_job(self.set_basic_info) - - if self._subscriptions: - raise RuntimeError( - f"Attempted to attach subscriptions to player: {self.soco} " - f"when existing subscriptions exist: {self._subscriptions}" - ) - - subscriptions = [ - self._subscribe(getattr(self.soco, service), self.async_dispatch_event) - for service in SUBSCRIPTION_SERVICES - ] - await asyncio.gather(*subscriptions) - except SoCoException as ex: - _LOGGER.warning("Could not connect %s: %s", self.zone_name, ex) - return False - return True + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + results = await asyncio.gather(*subscriptions, return_exceptions=True) + for result in results: + self.log_subscription_result( + result, "Creating subscription", logging.WARNING + ) async def _subscribe( self, target: SubscriptionBase, sub_callback: Callable @@ -399,49 +372,24 @@ async def async_unsubscribe(self) -> None: return_exceptions=True, ) for result in results: - if isinstance(result, asyncio.exceptions.TimeoutError): - message = "Request timed out" - exc_info = None - elif isinstance(result, Exception): - message = result - exc_info = result if not str(result) else None - else: - continue - _LOGGER.debug( - "Unsubscribe failed for %s: %s", - self.zone_name, - message, - exc_info=exc_info, - ) + self.log_subscription_result(result, "Unsubscribe") self._subscriptions = [] @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - self.hass.async_create_task(self.async_resubscribe(exception)) + self.hass.async_create_task(self._async_renew_failed(exception)) - async def async_resubscribe(self, exception: Exception) -> None: - """Attempt to resubscribe when a renewal failure is detected.""" - if not self._resubscription_lock: - self._resubscription_lock = asyncio.Lock() + async def _async_renew_failed(self, exception: Exception) -> None: + """Mark the speaker as offline after a subscription renewal failure. - async with self._resubscription_lock: - if not self.available: - return + This is to reset the state to allow a future clean subscription attempt. + """ + if not self.available: + return - if isinstance(exception, asyncio.exceptions.TimeoutError): - message = "Request timed out" - exc_info = None - else: - message = exception - exc_info = exception if not str(exception) else None - _LOGGER.warning( - "Subscription renewals for %s failed, marking unavailable: %s", - self.zone_name, - message, - exc_info=exc_info, - ) - await self.async_offline() + self.log_subscription_result(exception, "Subscription renewal", logging.WARNING) + await self.async_offline() @callback def async_dispatch_event(self, event: SonosEvent) -> None: @@ -508,7 +456,18 @@ def async_dispatch_media_update(self, event: SonosEvent) -> None: if crossfade := event.variables.get("current_crossfade_mode"): self.cross_fade = bool(int(crossfade)) - self.hass.async_add_executor_job(self.update_media, event) + # Missing transport_state indicates a transient error + if (new_status := event.variables.get("transport_state")) is None: + return + + # Ignore transitions, we should get the target state soon + if new_status == SONOS_STATE_TRANSITIONING: + return + + self.event_stats.process(event) + self.hass.async_add_executor_job( + self.media.update_media_from_event, event.variables + ) @callback def async_update_volume(self, event: SonosEvent) -> None: @@ -554,39 +513,48 @@ def speaker_activity(self, source): async def async_check_activity(self, now: datetime.datetime) -> None: """Validate availability of the speaker based on recent activity.""" + if not self.available: + return if time.monotonic() - self._last_activity < AVAILABILITY_TIMEOUT: return try: - _ = await self.hass.async_add_executor_job(getattr, self.soco, "volume") - except (OSError, SoCoException): - pass + # Make a short-timeout call as a final check + # before marking this speaker as unavailable + await self.hass.async_add_executor_job( + partial( + self.soco.renderingControl.GetVolume, + [("InstanceID", 0), ("Channel", "Master")], + timeout=1, + ) + ) + except OSError: + _LOGGER.warning( + "No recent activity and cannot reach %s, marking unavailable", + self.zone_name, + ) + await self.async_offline() else: self.speaker_activity("timeout poll") - return + async def async_offline(self) -> None: + """Handle removal of speaker when unavailable.""" if not self.available: return - _LOGGER.debug( - "No recent activity and cannot reach %s, marking unavailable", - self.zone_name, - ) - await self.async_offline() - - async def async_offline(self) -> None: - """Handle removal of speaker when unavailable.""" self.available = False + self.async_write_entity_states() + self._share_link_plugin = None if self._poll_timer: self._poll_timer() self._poll_timer = None - await self.async_unsubscribe() + async with self._subscription_lock: + await self.async_unsubscribe() self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) - self.async_write_entity_states() async def async_vanished(self, reason: str) -> None: """Handle removal of speaker when marked as vanished.""" @@ -599,8 +567,8 @@ async def async_vanished(self, reason: str) -> None: async def async_rebooted(self, soco: SoCo) -> None: """Handle a detected speaker reboot.""" - _LOGGER.warning( - "%s rebooted or lost network connectivity, reconnecting with %s", + _LOGGER.debug( + "%s rebooted, reconnecting with %s", self.zone_name, soco, ) @@ -713,7 +681,9 @@ def async_update_groups(self, event: SonosEvent) -> None: if xml := event.variables.get("zone_group_state"): zgs = ET.fromstring(xml) for vanished_device in zgs.find("VanishedDevices") or []: - if (reason := vanished_device.get("Reason")) != "sleeping": + if ( + reason := vanished_device.get("Reason") + ) not in SUPPORTED_VANISH_REASONS: _LOGGER.debug( "Ignoring %s marked %s as vanished with reason: %s", self.zone_name, @@ -1044,188 +1014,8 @@ def _test_groups(groups: list[list[SonosSpeaker]]) -> bool: # # Media and playback state handlers # - @soco_error(raise_on_err=False) + @soco_error() def update_volume(self) -> None: """Update information about current volume settings.""" self.volume = self.soco.volume self.muted = self.soco.mute - self.night_mode = self.soco.night_mode - self.dialog_level = self.soco.dialog_level - - try: - self.cross_fade = self.soco.cross_fade - except SoCoSlaveException: - pass - - @soco_error() - def update_media(self, event: SonosEvent | None = None) -> None: - """Update information about currently playing media.""" - variables = event.variables if event else {} - - if "transport_state" in variables: - # If the transport has an error then transport_state will - # not be set - new_status = variables["transport_state"] - else: - transport_info = self.soco.get_current_transport_info() - new_status = transport_info["current_transport_state"] - - # Ignore transitions, we should get the target state soon - if new_status == SONOS_STATE_TRANSITIONING: - return - - if event: - self.event_stats.process(event) - - self.media.clear() - update_position = new_status != self.media.playback_status - self.media.playback_status = new_status - - if "transport_state" in variables: - self.media.play_mode = variables["current_play_mode"] - track_uri = ( - variables["enqueued_transport_uri"] or variables["current_track_uri"] - ) - music_source = self.soco.music_source_from_uri(track_uri) - if uri_meta_data := variables.get("enqueued_transport_uri_meta_data"): - if isinstance(uri_meta_data, DidlPlaylistContainer): - self.media.playlist_name = uri_meta_data.title - else: - self.media.play_mode = self.soco.play_mode - music_source = self.soco.music_source - - if music_source == MUSIC_SRC_TV: - self.update_media_linein(SOURCE_TV) - elif music_source == MUSIC_SRC_LINE_IN: - self.update_media_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - if not track_info["uri"]: - self.media.clear_position() - else: - self.media.uri = track_info["uri"] - self.media.artist = track_info.get("artist") - self.media.album_name = track_info.get("album") - self.media.title = track_info.get("title") - - if music_source == MUSIC_SRC_RADIO: - self.update_media_radio(variables) - else: - self.update_media_music(track_info) - self.update_media_position(update_position, track_info) - - self.write_entity_states() - - # Also update slaves - speakers = self.hass.data[DATA_SONOS].discovered.values() - for speaker in speakers: - if speaker.coordinator == self: - speaker.write_entity_states() - - def update_media_linein(self, source: str) -> None: - """Update state when playing from line-in/tv.""" - self.media.clear_position() - - self.media.title = source - self.media.source_name = source - - def update_media_radio(self, variables: dict) -> None: - """Update state when streaming radio.""" - self.media.clear_position() - radio_title = None - - if current_track_metadata := variables.get("current_track_meta_data"): - if album_art_uri := getattr(current_track_metadata, "album_art_uri", None): - self.media.image_url = self.media.library.build_album_art_full_uri( - album_art_uri - ) - if not self.media.artist: - self.media.artist = getattr(current_track_metadata, "creator", None) - - # A missing artist implies metadata is incomplete, try a different method - if not self.media.artist: - radio_show = None - stream_content = None - if current_track_metadata.radio_show: - radio_show = current_track_metadata.radio_show.split(",")[0] - if not current_track_metadata.stream_content.startswith( - ("ZPSTR_", "TYPE=") - ): - stream_content = current_track_metadata.stream_content - radio_title = " • ".join(filter(None, [radio_show, stream_content])) - - if radio_title: - # Prefer the radio title created above - self.media.title = radio_title - elif uri_meta_data := variables.get("enqueued_transport_uri_meta_data"): - if isinstance(uri_meta_data, DidlAudioBroadcast) and ( - self.soco.music_source_from_uri(self.media.title) == MUSIC_SRC_RADIO - or ( - isinstance(self.media.title, str) - and isinstance(self.media.uri, str) - and ( - self.media.title in self.media.uri - or self.media.title in urllib.parse.unquote(self.media.uri) - ) - ) - ): - # Fall back to the radio channel name as a last resort - self.media.title = uri_meta_data.title - - media_info = self.soco.get_current_media_info() - self.media.channel = media_info["channel"] - - # Check if currently playing radio station is in favorites - fav = next( - ( - fav - for fav in self.favorites - if fav.reference.get_uri() == media_info["uri"] - ), - None, - ) - if fav: - self.media.source_name = fav.title - - def update_media_music(self, track_info: dict) -> None: - """Update state when playing music tracks.""" - self.media.image_url = track_info.get("album_art") - - playlist_position = int(track_info.get("playlist_position")) # type: ignore - if playlist_position > 0: - self.media.queue_position = playlist_position - 1 - - def update_media_position( - self, update_media_position: bool, track_info: dict - ) -> None: - """Update state when playing music tracks.""" - self.media.duration = _timespan_secs(track_info.get("duration")) - current_position = _timespan_secs(track_info.get("position")) - - if self.media.duration == 0: - self.media.clear_position() - return - - # player started reporting position? - if current_position is not None and self.media.position is None: - update_media_position = True - - # position jumped? - if current_position is not None and self.media.position is not None: - if self.media.playback_status == SONOS_STATE_PLAYING: - assert self.media.position_updated_at is not None - time_delta = dt_util.utcnow() - self.media.position_updated_at - time_diff = time_delta.total_seconds() - else: - time_diff = 0 - - calculated_position = self.media.position + time_diff - - if abs(calculated_position - current_position) > 1.5: - update_media_position = True - - if current_position is None: - self.media.clear_position() - elif update_media_position: - self.media.position = current_position - self.media.position_updated_at = dt_util.utcnow() diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 2ee8af61327a0..4ee3253c88959 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -5,7 +5,7 @@ import logging from typing import Any -from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException +from soco.exceptions import SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -23,8 +23,7 @@ SONOS_CREATE_ALARM, SONOS_CREATE_SWITCHES, ) -from .entity import SonosEntity -from .exception import SpeakerUnavailable +from .entity import SonosEntity, SonosPollingEntity from .helpers import soco_error from .speaker import SonosSpeaker @@ -144,7 +143,7 @@ async def _async_create_switches(speaker: SonosSpeaker) -> None: ) -class SonosSwitchEntity(SonosEntity, SwitchEntity): +class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): """Representation of a Sonos feature switch.""" def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None: @@ -164,17 +163,14 @@ def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None: self._attr_entity_registry_enabled_default = False self._attr_should_poll = True - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Handle polling for subscription-based switches when subscription fails.""" if not self.should_poll: - await self.hass.async_add_executor_job(self.update) - - @soco_error(raise_on_err=False) - def update(self) -> None: - """Fetch switch state if necessary.""" - if not self.available: - raise SpeakerUnavailable + await self.hass.async_add_executor_job(self.poll_state) + @soco_error() + def poll_state(self) -> None: + """Poll the current state of the switch.""" state = getattr(self.soco, self.feature_type) setattr(self.speaker, self.feature_type, state) @@ -245,7 +241,7 @@ def name(self) -> str: str(self.alarm.start_time)[0:5], ) - async def _async_poll(self) -> None: + async def _async_fallback_poll(self) -> None: """Call the central alarm polling method.""" await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() @@ -268,7 +264,6 @@ async def async_update_state(self) -> None: if not self.async_check_if_available(): return - _LOGGER.debug("Updating alarm: %s", self.entity_id) if self.speaker.soco.uid != self.alarm.zone.uid: self.speaker = self.hass.data[DATA_SONOS].discovered.get( self.alarm.zone.uid @@ -351,14 +346,11 @@ def turn_off(self, **kwargs: Any) -> None: """Turn alarm switch off.""" self._handle_switch_on_off(turn_on=False) + @soco_error() def _handle_switch_on_off(self, turn_on: bool) -> None: """Handle turn on/off of alarm switch.""" - try: - _LOGGER.debug("Toggling the state of %s", self.entity_id) - self.alarm.enabled = turn_on - self.alarm.save() - except (OSError, SoCoException, SoCoUPnPException) as exc: - _LOGGER.error("Could not update %s: %s", self.entity_id, exc) + self.alarm.enabled = turn_on + self.alarm.save() @callback diff --git a/homeassistant/components/sonos/translations/el.json b/homeassistant/components/sonos/translations/el.json index fa7c08d30dc61..d538ac09c4b7f 100644 --- a/homeassistant/components/sonos/translations/el.json +++ b/homeassistant/components/sonos/translations/el.json @@ -1,5 +1,10 @@ { "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", + "not_sonos_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 Sonos", + "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." + }, "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Sonos;" diff --git a/homeassistant/components/sonos/translations/pt-BR.json b/homeassistant/components/sonos/translations/pt-BR.json index f2467135d3555..527d0046b7c07 100644 --- a/homeassistant/components/sonos/translations/pt-BR.json +++ b/homeassistant/components/sonos/translations/pt-BR.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_sonos_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Sonos", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { - "description": "Voc\u00ea quer configurar o Sonos?" + "description": "Deseja configurar o Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/uk.json b/homeassistant/components/sonos/translations/uk.json index aff6c9f59b179..64d4af145c408 100644 --- a/homeassistant/components/sonos/translations/uk.json +++ b/homeassistant/components/sonos/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json index 08434f16c1576..26e802b44c763 100644 --- a/homeassistant/components/sonos/translations/zh-Hant.json +++ b/homeassistant/components/sonos/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "not_sonos_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Sonos \u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 07819b7b63941..721b0e90402f9 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sony_projector", "requirements": ["pysdcp==1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysdcp"] } diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 2b8c2fb5477a9..15091ec04f71a 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -5,5 +5,6 @@ "requirements": ["libsoundtouch==0.8"], "after_dependencies": ["zeroconf"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["libsoundtouch"] } diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 9906a4025a562..088ddc8dd7bce 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/spc", "requirements": ["pyspcwebgw==0.4.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pyspcwebgw"] } diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 735c656134b26..e2455ad63df09 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -36,14 +36,14 @@ class SpeedtestSensorEntityDescription(SensorEntityDescription): name="Download", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - value=lambda value: round(value / 10 ** 6, 2), + value=lambda value: round(value / 10**6, 2), ), SpeedtestSensorEntityDescription( key="upload", name="Upload", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - value=lambda value: round(value / 10 ** 6, 2), + value=lambda value: round(value / 10**6, 2), ), ) diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index a8ac5c9292451..096e339732cbc 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -1,14 +1,22 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "wrong_server_id": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf" + }, + "step": { + "user": { + "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;" + } } }, "options": { "step": { "init": { "data": { - "manual": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2" + "manual": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", + "scan_interval": "\u03a3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)", + "server_name": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03bf\u03ba\u03b9\u03bc\u03ae\u03c2" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json new file mode 100644 index 0000000000000..0498a2fad6de1 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "wrong_server_id": "O ID do servidor n\u00e3o \u00e9 v\u00e1lido" + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Desativar atualiza\u00e7\u00e3o autom\u00e1tica", + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (minutos)", + "server_name": "Selecione o servidor de teste" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json index 89ef24440d13e..e54ed1da92ee2 100644 --- a/homeassistant/components/speedtestdotnet/translations/uk.json +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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.", "wrong_server_id": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430." }, "step": { diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index e88b4ec39233d..49b8b0cfb2097 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548" }, "step": { diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index b80fa0926cde0..56cd6876e9fbd 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -5,5 +5,6 @@ "requirements": ["spiderpy==1.6.1"], "codeowners": ["@peternijssen"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["spiderpy"] } \ No newline at end of file diff --git a/homeassistant/components/spider/translations/el.json b/homeassistant/components/spider/translations/el.json new file mode 100644 index 0000000000000..042f6c824ce96 --- /dev/null +++ b/homeassistant/components/spider/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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": { + "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": { + "user": { + "data": { + "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" + }, + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/pt-BR.json b/homeassistant/components/spider/translations/pt-BR.json new file mode 100644 index 0000000000000..3c281b4754cc5 --- /dev/null +++ b/homeassistant/components/spider/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Entrar com a conta mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/sk.json b/homeassistant/components/spider/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/spider/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/uk.json b/homeassistant/components/spider/translations/uk.json index b8be2a1488791..52e7158c40ac3 100644 --- a/homeassistant/components/spider/translations/uk.json +++ b/homeassistant/components/spider/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", diff --git a/homeassistant/components/spider/translations/zh-Hant.json b/homeassistant/components/spider/translations/zh-Hant.json index ce15c28f47b77..711ed2f62ff0d 100644 --- a/homeassistant/components/spider/translations/zh-Hant.json +++ b/homeassistant/components/spider/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 09a128c9b72fa..7ada3ea2a3775 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/splunk", "requirements": ["hass_splunk==0.1.1"], "codeowners": ["@Bre77"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["hass_splunk"] } diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 446c2ace82cdb..c057ea240c040 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,6 +1,7 @@ """The spotify integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -9,7 +10,6 @@ from spotipy import Spotify, SpotifyException import voluptuous as vol -from homeassistant.components.media_player import BrowseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CREDENTIALS, @@ -28,17 +28,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow -from .const import ( - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_DEVICES, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, - LOGGER, - MEDIA_PLAYER_PREFIX, - SPOTIFY_SCOPES, +from .browse_media import async_browse_media +from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES +from .util import ( + is_spotify_media_type, + resolve_spotify_media_type, + spotify_uri_from_media_browser_url, ) -from .media_player import async_browse_media_internal CONFIG_SCHEMA = vol.Schema( { @@ -55,31 +51,23 @@ PLATFORMS = [Platform.MEDIA_PLAYER] -def is_spotify_media_type(media_content_type): - """Return whether the media_content_type is a valid Spotify media_id.""" - return media_content_type.startswith(MEDIA_PLAYER_PREFIX) +__all__ = [ + "async_browse_media", + "DOMAIN", + "spotify_uri_from_media_browser_url", + "is_spotify_media_type", + "resolve_spotify_media_type", +] -def resolve_spotify_media_type(media_content_type): - """Return actual spotify media_content_type.""" - return media_content_type[len(MEDIA_PLAYER_PREFIX) :] +@dataclass +class HomeAssistantSpotifyData: + """Spotify data stored in the Home Assistant data object.""" - -async def async_browse_media( - hass, media_content_type, media_content_id, *, can_play_artist=True -): - """Browse Spotify media.""" - if not (info := next(iter(hass.data[DOMAIN].values()), None)): - raise BrowseError("No Spotify accounts available") - return await async_browse_media_internal( - hass, - info[DATA_SPOTIFY_CLIENT], - info[DATA_SPOTIFY_SESSION], - info[DATA_SPOTIFY_ME], - media_content_type, - media_content_id, - can_play_artist=can_play_artist, - ) + client: Spotify + current_user: dict[str, Any] + devices: DataUpdateCoordinator + session: OAuth2Session async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -120,6 +108,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SpotifyException as err: raise ConfigEntryNotReady from err + if not current_user: + raise ConfigEntryNotReady + async def _update_devices() -> list[dict[str, Any]]: if not session.valid_token: await session.async_ensure_token_valid() @@ -151,12 +142,12 @@ async def _update_devices() -> list[dict[str, Any]]: await device_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_SPOTIFY_CLIENT: spotify, - DATA_SPOTIFY_DEVICES: device_coordinator, - DATA_SPOTIFY_ME: current_user, - DATA_SPOTIFY_SESSION: session, - } + hass.data[DOMAIN][entry.entry_id] = HomeAssistantSpotifyData( + client=spotify, + current_user=current_user, + devices=device_coordinator, + session=session, + ) if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed @@ -167,12 +158,6 @@ async def _update_devices() -> list[dict[str, Any]]: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Spotify config entry.""" - # Unload entities for this entry/device. - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - # Cleanup - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] return unload_ok diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py new file mode 100644 index 0000000000000..db2379a57fc37 --- /dev/null +++ b/homeassistant/components/spotify/browse_media.py @@ -0,0 +1,482 @@ +"""Support for Spotify media browsing.""" +from __future__ import annotations + +from functools import partial +import logging +from typing import Any + +from spotipy import Spotify +import yarl + +from homeassistant.backports.enum import StrEnum +from homeassistant.components.media_player import BrowseError, BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_APP, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_PODCAST, + MEDIA_CLASS_TRACK, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session + +from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES +from .util import fetch_image_url + +BROWSE_LIMIT = 48 + + +_LOGGER = logging.getLogger(__name__) + + +class BrowsableMedia(StrEnum): + """Enum of browsable media.""" + + CURRENT_USER_PLAYLISTS = "current_user_playlists" + CURRENT_USER_FOLLOWED_ARTISTS = "current_user_followed_artists" + CURRENT_USER_SAVED_ALBUMS = "current_user_saved_albums" + CURRENT_USER_SAVED_TRACKS = "current_user_saved_tracks" + CURRENT_USER_SAVED_SHOWS = "current_user_saved_shows" + CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played" + CURRENT_USER_TOP_ARTISTS = "current_user_top_artists" + CURRENT_USER_TOP_TRACKS = "current_user_top_tracks" + CATEGORIES = "categories" + FEATURED_PLAYLISTS = "featured_playlists" + NEW_RELEASES = "new_releases" + + +LIBRARY_MAP = { + BrowsableMedia.CURRENT_USER_PLAYLISTS.value: "Playlists", + BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS.value: "Artists", + BrowsableMedia.CURRENT_USER_SAVED_ALBUMS.value: "Albums", + BrowsableMedia.CURRENT_USER_SAVED_TRACKS.value: "Tracks", + BrowsableMedia.CURRENT_USER_SAVED_SHOWS.value: "Podcasts", + BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played", + BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists", + BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks", + BrowsableMedia.CATEGORIES.value: "Categories", + BrowsableMedia.FEATURED_PLAYLISTS.value: "Featured Playlists", + BrowsableMedia.NEW_RELEASES.value: "New Releases", +} + +CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = { + BrowsableMedia.CURRENT_USER_PLAYLISTS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + BrowsableMedia.CURRENT_USER_SAVED_ALBUMS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ALBUM, + }, + BrowsableMedia.CURRENT_USER_SAVED_TRACKS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + BrowsableMedia.CURRENT_USER_SAVED_SHOWS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PODCAST, + }, + BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + BrowsableMedia.FEATURED_PLAYLISTS.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + BrowsableMedia.CATEGORIES.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_GENRE, + }, + "category_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + BrowsableMedia.NEW_RELEASES.value: { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ALBUM, + }, + MEDIA_TYPE_PLAYLIST: { + "parent": MEDIA_CLASS_PLAYLIST, + "children": MEDIA_CLASS_TRACK, + }, + MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, + MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None}, + MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE}, + MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None}, +} + + +class MissingMediaInformation(BrowseError): + """Missing media required information.""" + + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + +async def async_browse_media( + hass: HomeAssistant, + media_content_type: str | None, + media_content_id: str | None, + *, + can_play_artist: bool = True, +) -> BrowseMedia: + """Browse Spotify media.""" + parsed_url = None + info = None + + # Check if caller is requesting the root nodes + if media_content_type is None and media_content_id is None: + children = [] + for config_entry_id, info in hass.data[DOMAIN].items(): + config_entry = hass.config_entries.async_get_entry(config_entry_id) + assert config_entry is not None + children.append( + BrowseMedia( + title=config_entry.title, + media_class=MEDIA_CLASS_APP, + media_content_id=f"{MEDIA_PLAYER_PREFIX}{config_entry_id}", + media_content_type=f"{MEDIA_PLAYER_PREFIX}library", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + ) + ) + return BrowseMedia( + title="Spotify", + media_class=MEDIA_CLASS_APP, + media_content_id=MEDIA_PLAYER_PREFIX, + media_content_type="spotify", + thumbnail="https://brands.home-assistant.io/_/spotify/logo.png", + can_play=False, + can_expand=True, + children=children, + ) + + if media_content_id is None or not media_content_id.startswith(MEDIA_PLAYER_PREFIX): + raise BrowseError("Invalid Spotify URL specified") + + # Check for config entry specifier, and extract Spotify URI + parsed_url = yarl.URL(media_content_id) + if (info := hass.data[DOMAIN].get(parsed_url.host)) is None: + raise BrowseError("Invalid Spotify account specified") + media_content_id = parsed_url.name + + result = await async_browse_media_internal( + hass, + info.client, + info.session, + info.current_user, + media_content_type, + media_content_id, + can_play_artist=can_play_artist, + ) + + # Build new URLs with config entry specifyers + result.media_content_id = str(parsed_url.with_name(result.media_content_id)) + if result.children: + for child in result.children: + child.media_content_id = str(parsed_url.with_name(child.media_content_id)) + return result + + +async def async_browse_media_internal( + hass: HomeAssistant, + spotify: Spotify, + session: OAuth2Session, + current_user: dict[str, Any], + media_content_type: str | None, + media_content_id: str | None, + *, + can_play_artist: bool = True, +) -> BrowseMedia: + """Browse spotify media.""" + if media_content_type in (None, f"{MEDIA_PLAYER_PREFIX}library"): + return await hass.async_add_executor_job( + partial(library_payload, can_play_artist=can_play_artist) + ) + + if not session.valid_token: + await session.async_ensure_token_valid() + await hass.async_add_executor_job( + spotify.set_auth, session.token["access_token"] + ) + + # Strip prefix + if media_content_type: + media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :] + + payload = { + "media_content_type": media_content_type, + "media_content_id": media_content_id, + } + response = await hass.async_add_executor_job( + partial( + build_item_response, + spotify, + current_user, + payload, + can_play_artist=can_play_artist, + ) + ) + if response is None: + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + return response + + +def build_item_response( # noqa: C901 + spotify: Spotify, + user: dict[str, Any], + payload: dict[str, str | None], + *, + can_play_artist: bool, +) -> BrowseMedia | None: + """Create response payload for the provided media query.""" + media_content_type = payload["media_content_type"] + media_content_id = payload["media_content_id"] + + if media_content_type is None or media_content_id is None: + return None + + title = None + image = None + media: dict[str, Any] | None = None + items = [] + + if media_content_type == BrowsableMedia.CURRENT_USER_PLAYLISTS: + if media := spotify.current_user_playlists(limit=BROWSE_LIMIT): + items = media.get("items", []) + elif media_content_type == BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: + if media := spotify.current_user_followed_artists(limit=BROWSE_LIMIT): + items = media.get("artists", {}).get("items", []) + elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: + if media := spotify.current_user_saved_albums(limit=BROWSE_LIMIT): + items = [item["album"] for item in media.get("items", [])] + elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_TRACKS: + if media := spotify.current_user_saved_tracks(limit=BROWSE_LIMIT): + items = [item["track"] for item in media.get("items", [])] + elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_SHOWS: + if media := spotify.current_user_saved_shows(limit=BROWSE_LIMIT): + items = [item["show"] for item in media.get("items", [])] + elif media_content_type == BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: + if media := spotify.current_user_recently_played(limit=BROWSE_LIMIT): + items = [item["track"] for item in media.get("items", [])] + elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_ARTISTS: + if media := spotify.current_user_top_artists(limit=BROWSE_LIMIT): + items = media.get("items", []) + elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS: + if media := spotify.current_user_top_tracks(limit=BROWSE_LIMIT): + items = media.get("items", []) + elif media_content_type == BrowsableMedia.FEATURED_PLAYLISTS: + if media := spotify.featured_playlists( + country=user["country"], limit=BROWSE_LIMIT + ): + items = media.get("playlists", {}).get("items", []) + elif media_content_type == BrowsableMedia.CATEGORIES: + if media := spotify.categories(country=user["country"], limit=BROWSE_LIMIT): + items = media.get("categories", {}).get("items", []) + elif media_content_type == "category_playlists": + if ( + media := spotify.category_playlists( + category_id=media_content_id, + country=user["country"], + limit=BROWSE_LIMIT, + ) + ) and (category := spotify.category(media_content_id, country=user["country"])): + title = category.get("name") + image = fetch_image_url(category, key="icons") + items = media.get("playlists", {}).get("items", []) + elif media_content_type == BrowsableMedia.NEW_RELEASES: + if media := spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT): + items = media.get("albums", {}).get("items", []) + elif media_content_type == MEDIA_TYPE_PLAYLIST: + if media := spotify.playlist(media_content_id): + items = [item["track"] for item in media.get("tracks", {}).get("items", [])] + elif media_content_type == MEDIA_TYPE_ALBUM: + if media := spotify.album(media_content_id): + items = media.get("tracks", {}).get("items", []) + elif media_content_type == MEDIA_TYPE_ARTIST: + if (media := spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT)) and ( + artist := spotify.artist(media_content_id) + ): + title = artist.get("name") + image = fetch_image_url(artist) + items = media.get("items", []) + elif media_content_type == MEDIA_TYPE_SHOW: + if (media := spotify.show_episodes(media_content_id, limit=BROWSE_LIMIT)) and ( + show := spotify.show(media_content_id) + ): + title = show.get("name") + image = fetch_image_url(show) + items = media.get("items", []) + + if media is None: + return None + + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + except KeyError: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + return None + + if media_content_type == BrowsableMedia.CATEGORIES: + media_item = BrowseMedia( + can_expand=True, + can_play=False, + children_media_class=media_class["children"], + media_class=media_class["parent"], + media_content_id=media_content_id, + media_content_type=f"{MEDIA_PLAYER_PREFIX}{media_content_type}", + title=LIBRARY_MAP.get(media_content_id, "Unknown"), + ) + + media_item.children = [] + for item in items: + try: + item_id = item["id"] + except KeyError: + _LOGGER.debug("Missing ID for media item: %s", item) + continue + media_item.children.append( + BrowseMedia( + can_expand=True, + can_play=False, + children_media_class=MEDIA_CLASS_TRACK, + media_class=MEDIA_CLASS_PLAYLIST, + media_content_id=item_id, + media_content_type=f"{MEDIA_PLAYER_PREFIX}category_playlists", + thumbnail=fetch_image_url(item, key="icons"), + title=item.get("name"), + ) + ) + return media_item + + if title is None: + title = LIBRARY_MAP.get(media_content_id, "Unknown") + if "name" in media: + title = media["name"] + + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and ( + media_content_type != MEDIA_TYPE_ARTIST or can_play_artist + ) + + browse_media = BrowseMedia( + can_expand=True, + can_play=can_play, + children_media_class=media_class["children"], + media_class=media_class["parent"], + media_content_id=media_content_id, + media_content_type=f"{MEDIA_PLAYER_PREFIX}{media_content_type}", + thumbnail=image, + title=title, + ) + + browse_media.children = [] + for item in items: + try: + browse_media.children.append( + item_payload(item, can_play_artist=can_play_artist) + ) + except (MissingMediaInformation, UnknownMediaType): + continue + + if "images" in media: + browse_media.thumbnail = fetch_image_url(media) + + return browse_media + + +def item_payload(item: dict[str, Any], *, can_play_artist: bool) -> BrowseMedia: + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + try: + media_type = item["type"] + media_id = item["uri"] + except KeyError as err: + _LOGGER.debug("Missing type or URI for media item: %s", item) + raise MissingMediaInformation from err + + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_type) + raise UnknownMediaType from err + + can_expand = media_type not in [ + MEDIA_TYPE_TRACK, + MEDIA_TYPE_EPISODE, + ] + + can_play = media_type in PLAYABLE_MEDIA_TYPES and ( + media_type != MEDIA_TYPE_ARTIST or can_play_artist + ) + + browse_media = BrowseMedia( + can_expand=can_expand, + can_play=can_play, + children_media_class=media_class["children"], + media_class=media_class["parent"], + media_content_id=media_id, + media_content_type=f"{MEDIA_PLAYER_PREFIX}{media_type}", + title=item.get("name", "Unknown"), + ) + + if "images" in item: + browse_media.thumbnail = fetch_image_url(item) + elif MEDIA_TYPE_ALBUM in item: + browse_media.thumbnail = fetch_image_url(item[MEDIA_TYPE_ALBUM]) + + return browse_media + + +def library_payload(*, can_play_artist: bool) -> BrowseMedia: + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + browse_media = BrowseMedia( + can_expand=True, + can_play=False, + children_media_class=MEDIA_CLASS_DIRECTORY, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="library", + media_content_type=f"{MEDIA_PLAYER_PREFIX}library", + title="Media Library", + ) + + browse_media.children = [] + for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]: + browse_media.children.append( + item_payload( + {"name": item["name"], "type": item["type"], "uri": item["type"]}, + can_play_artist=can_play_artist, + ) + ) + return browse_media diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index 33e5e67a24414..bd01fa64acb82 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification +from homeassistant.config_entries import ConfigEntry from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -22,10 +23,7 @@ class SpotifyFlowHandler( DOMAIN = DOMAIN VERSION = 1 - def __init__(self) -> None: - """Instantiate config flow.""" - super().__init__() - self.entry: dict[str, Any] | None = None + reauth_entry: ConfigEntry | None = None @property def logger(self) -> logging.Logger: @@ -48,7 +46,7 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: name = data["id"] = current_user["id"] - if self.entry and self.entry["id"] != current_user["id"]: + if self.reauth_entry and self.reauth_entry.data["id"] != current_user["id"]: return self.async_abort(reason="reauth_account_mismatch") if current_user.get("display_name"): @@ -61,8 +59,9 @@ async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: async def async_step_reauth(self, entry: dict[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" - if entry: - self.entry = entry + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) persistent_notification.async_create( self.hass, @@ -77,16 +76,18 @@ async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" - if user_input is None: + if self.reauth_entry is None: + return self.async_abort(reason="reauth_account_mismatch") + + if user_input is None and self.reauth_entry: return self.async_show_form( step_id="reauth_confirm", - description_placeholders={"account": self.entry["id"]}, + description_placeholders={"account": self.reauth_entry.data["id"]}, data_schema=vol.Schema({}), errors={}, ) persistent_notification.async_dismiss(self.hass, "spotify_reauth") - return await self.async_step_pick_implementation( - user_input={"implementation": self.entry["auth_implementation"]} + user_input={"implementation": self.reauth_entry.data["auth_implementation"]} ) diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 0ed7cd2412e43..ad73262921b1b 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -2,15 +2,18 @@ import logging +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +) + DOMAIN = "spotify" LOGGER = logging.getLogger(__package__) -DATA_SPOTIFY_CLIENT = "spotify_client" -DATA_SPOTIFY_DEVICES = "spotify_devices" -DATA_SPOTIFY_ME = "spotify_me" -DATA_SPOTIFY_SESSION = "spotify_session" - SPOTIFY_SCOPES = [ # Needed to be able to control playback "user-modify-playback-state", @@ -29,3 +32,13 @@ ] MEDIA_PLAYER_PREFIX = "spotify://" +MEDIA_TYPE_SHOW = "show" + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_SHOW, + MEDIA_TYPE_TRACK, +] diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 402083aa25d76..9dcf1fee6dc1f 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -8,5 +8,6 @@ "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["spotipy"] } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 06efb5558d1d4..2b62fdd78c49c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -4,26 +4,14 @@ from asyncio import run_coroutine_threadsafe import datetime as dt from datetime import timedelta -from functools import partial import logging import requests -from spotipy import Spotify, SpotifyException +from spotipy import SpotifyException from yarl import URL -from homeassistant.backports.enum import StrEnum from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_GENRE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_PODCAST, - MEDIA_CLASS_TRACK, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -43,32 +31,19 @@ SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, ) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ID, - CONF_NAME, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utc_from_timestamp -from .const import ( - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_DEVICES, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, - MEDIA_PLAYER_PREFIX, - SPOTIFY_SCOPES, -) +from . import HomeAssistantSpotifyData +from .browse_media import async_browse_media_internal +from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES +from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) @@ -98,118 +73,6 @@ value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items() } -BROWSE_LIMIT = 48 - -MEDIA_TYPE_SHOW = "show" - -PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_SHOW, - MEDIA_TYPE_TRACK, -] - - -class BrowsableMedia(StrEnum): - """Enum of browsable media.""" - - CURRENT_USER_PLAYLISTS = "current_user_playlists" - CURRENT_USER_FOLLOWED_ARTISTS = "current_user_followed_artists" - CURRENT_USER_SAVED_ALBUMS = "current_user_saved_albums" - CURRENT_USER_SAVED_TRACKS = "current_user_saved_tracks" - CURRENT_USER_SAVED_SHOWS = "current_user_saved_shows" - CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played" - CURRENT_USER_TOP_ARTISTS = "current_user_top_artists" - CURRENT_USER_TOP_TRACKS = "current_user_top_tracks" - CATEGORIES = "categories" - FEATURED_PLAYLISTS = "featured_playlists" - NEW_RELEASES = "new_releases" - - -LIBRARY_MAP = { - BrowsableMedia.CURRENT_USER_PLAYLISTS: "Playlists", - BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: "Artists", - BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: "Albums", - BrowsableMedia.CURRENT_USER_SAVED_TRACKS: "Tracks", - BrowsableMedia.CURRENT_USER_SAVED_SHOWS: "Podcasts", - BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: "Recently played", - BrowsableMedia.CURRENT_USER_TOP_ARTISTS: "Top Artists", - BrowsableMedia.CURRENT_USER_TOP_TRACKS: "Top Tracks", - BrowsableMedia.CATEGORIES: "Categories", - BrowsableMedia.FEATURED_PLAYLISTS: "Featured Playlists", - BrowsableMedia.NEW_RELEASES: "New Releases", -} - -CONTENT_TYPE_MEDIA_CLASS = { - BrowsableMedia.CURRENT_USER_PLAYLISTS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, - }, - BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ARTIST, - }, - BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ALBUM, - }, - BrowsableMedia.CURRENT_USER_SAVED_TRACKS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, - }, - BrowsableMedia.CURRENT_USER_SAVED_SHOWS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PODCAST, - }, - BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, - }, - BrowsableMedia.CURRENT_USER_TOP_ARTISTS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ARTIST, - }, - BrowsableMedia.CURRENT_USER_TOP_TRACKS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_TRACK, - }, - BrowsableMedia.FEATURED_PLAYLISTS: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, - }, - BrowsableMedia.CATEGORIES: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_GENRE, - }, - "category_playlists": { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_PLAYLIST, - }, - BrowsableMedia.NEW_RELEASES: { - "parent": MEDIA_CLASS_DIRECTORY, - "children": MEDIA_CLASS_ALBUM, - }, - MEDIA_TYPE_PLAYLIST: { - "parent": MEDIA_CLASS_PLAYLIST, - "children": MEDIA_CLASS_TRACK, - }, - MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, - MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, - MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None}, - MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE}, - MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None}, -} - - -class MissingMediaInformation(BrowseError): - """Missing media required information.""" - - -class UnknownMediaType(BrowseError): - """Unknown media type.""" - async def async_setup_entry( hass: HomeAssistant, @@ -220,7 +83,7 @@ async def async_setup_entry( spotify = SpotifyMediaPlayer( hass.data[DOMAIN][entry.entry_id], entry.data[CONF_ID], - entry.data[CONF_NAME], + entry.title, ) async_add_entities([spotify], True) @@ -257,61 +120,35 @@ class SpotifyMediaPlayer(MediaPlayerEntity): def __init__( self, - spotify_data, + data: HomeAssistantSpotifyData, user_id: str, name: str, ) -> None: """Initialize.""" self._id = user_id - self._spotify_data = spotify_data - self._name = f"Spotify {name}" - self._scope_ok = set(self._session.token["scope"].split(" ")).issuperset( - SPOTIFY_SCOPES - ) + self.data = data - self._currently_playing: dict | None = {} - self._playlist: dict | None = None - - self._attr_name = self._name + self._attr_name = f"Spotify {name}" self._attr_unique_id = user_id - @property - def _me(self) -> dict: - """Return spotify user info.""" - return self._spotify_data[DATA_SPOTIFY_ME] + if self.data.current_user["product"] == "premium": + self._attr_supported_features = SUPPORT_SPOTIFY - @property - def _session(self) -> OAuth2Session: - """Return spotify session.""" - return self._spotify_data[DATA_SPOTIFY_SESSION] - - @property - def _spotify(self) -> Spotify: - """Return spotify API.""" - return self._spotify_data[DATA_SPOTIFY_CLIENT] - - @property - def _devices(self) -> list: - """Return spotify devices.""" - return self._spotify_data[DATA_SPOTIFY_DEVICES].data - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this entity.""" - model = "Spotify Free" - if self._me is not None: - product = self._me["product"] - model = f"Spotify {product}" - - return DeviceInfo( - identifiers={(DOMAIN, self._id)}, + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, user_id)}, manufacturer="Spotify AB", - model=model, - name=self._name, + model=f"Spotify {data.current_user['product']}", + name=f"Spotify {name}", entry_type=DeviceEntryType.SERVICE, configuration_url="https://open.spotify.com", ) + self._scope_ok = set(data.session.token["scope"].split(" ")).issuperset( + SPOTIFY_SCOPES + ) + self._currently_playing: dict | None = {} + self._playlist: dict | None = None + @property def state(self) -> str | None: """Return the playback state.""" @@ -324,23 +161,30 @@ def state(self) -> str | None: @property def volume_level(self) -> float | None: """Return the device volume.""" + if not self._currently_playing: + return None return self._currently_playing.get("device", {}).get("volume_percent", 0) / 100 @property def media_content_id(self) -> str | None: """Return the media URL.""" + if not self._currently_playing: + return None item = self._currently_playing.get("item") or {} return item.get("uri") @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - if self._currently_playing.get("item") is None: + if ( + self._currently_playing is None + or self._currently_playing.get("item") is None + ): return None return self._currently_playing["item"]["duration_ms"] / 1000 @property - def media_position(self) -> str | None: + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if not self._currently_playing: return None @@ -357,7 +201,8 @@ def media_position_updated_at(self) -> dt.datetime | None: def media_image_url(self) -> str | None: """Return the media image URL.""" if ( - self._currently_playing.get("item") is None + not self._currently_playing + or self._currently_playing.get("item") is None or not self._currently_playing["item"]["album"]["images"] ): return None @@ -366,13 +211,15 @@ def media_image_url(self) -> str | None: @property def media_title(self) -> str | None: """Return the media title.""" + if not self._currently_playing: + return None item = self._currently_playing.get("item") or {} return item.get("name") @property def media_artist(self) -> str | None: """Return the media artist.""" - if self._currently_playing.get("item") is None: + if not self._currently_playing or self._currently_playing.get("item") is None: return None return ", ".join( artist["name"] for artist in self._currently_playing["item"]["artists"] @@ -381,13 +228,15 @@ def media_artist(self) -> str | None: @property def media_album_name(self) -> str | None: """Return the media album.""" - if self._currently_playing.get("item") is None: + if not self._currently_playing or self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["album"]["name"] @property def media_track(self) -> int | None: """Track number of current playing media, music track only.""" + if not self._currently_playing: + return None item = self._currently_playing.get("item") or {} return item.get("track_number") @@ -401,62 +250,61 @@ def media_playlist(self): @property def source(self) -> str | None: """Return the current playback device.""" + if not self._currently_playing: + return None return self._currently_playing.get("device", {}).get("name") @property def source_list(self) -> list[str] | None: """Return a list of source devices.""" - if not self._devices: - return None - return [device["name"] for device in self._devices] + return [device["name"] for device in self.data.devices.data] @property - def shuffle(self) -> bool: + def shuffle(self) -> bool | None: """Shuffling state.""" - return bool(self._currently_playing.get("shuffle_state")) + if not self._currently_playing: + return None + return self._currently_playing.get("shuffle_state") @property def repeat(self) -> str | None: """Return current repeat mode.""" - repeat_state = self._currently_playing.get("repeat_state") + if ( + not self._currently_playing + or (repeat_state := self._currently_playing.get("repeat_state")) is None + ): + return None return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) - @property - def supported_features(self) -> int: - """Return the media player features that are supported.""" - if self._me["product"] != "premium": - return 0 - return SUPPORT_SPOTIFY - @spotify_exception_handler def set_volume_level(self, volume: int) -> None: """Set the volume level.""" - self._spotify.volume(int(volume * 100)) + self.data.client.volume(int(volume * 100)) @spotify_exception_handler def media_play(self) -> None: """Start or resume playback.""" - self._spotify.start_playback() + self.data.client.start_playback() @spotify_exception_handler def media_pause(self) -> None: """Pause playback.""" - self._spotify.pause_playback() + self.data.client.pause_playback() @spotify_exception_handler def media_previous_track(self) -> None: """Skip to previous track.""" - self._spotify.previous_track() + self.data.client.previous_track() @spotify_exception_handler def media_next_track(self) -> None: """Skip to next track.""" - self._spotify.next_track() + self.data.client.next_track() @spotify_exception_handler def media_seek(self, position): """Send seek command.""" - self._spotify.seek_track(int(position * 1000)) + self.data.client.seek_track(int(position * 1000)) @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs) -> None: @@ -478,17 +326,21 @@ def play_media(self, media_type: str, media_id: str, **kwargs) -> None: _LOGGER.error("Media type %s is not supported", media_type) return - if not self._currently_playing.get("device") and self._devices: - kwargs["device_id"] = self._devices[0].get("id") + if ( + self._currently_playing + and not self._currently_playing.get("device") + and self.data.devices.data + ): + kwargs["device_id"] = self.data.devices.data[0].get("id") - self._spotify.start_playback(**kwargs) + self.data.client.start_playback(**kwargs) @spotify_exception_handler def select_source(self, source: str) -> None: """Select playback device.""" - for device in self._devices: + for device in self.data.devices.data: if device["name"] == source: - self._spotify.transfer_playback( + self.data.client.transfer_playback( device["id"], self.state == STATE_PLAYING ) return @@ -496,14 +348,14 @@ def select_source(self, source: str) -> None: @spotify_exception_handler def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" - self._spotify.shuffle(shuffle) + self.data.client.shuffle(shuffle) @spotify_exception_handler def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: raise ValueError(f"Unsupported repeat mode: {repeat}") - self._spotify.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) + self.data.client.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) @spotify_exception_handler def update(self) -> None: @@ -511,13 +363,13 @@ def update(self) -> None: if not self.enabled: return - if not self._session.valid_token or self._spotify is None: + if not self.data.session.valid_token or self.data.client is None: run_coroutine_threadsafe( - self._session.async_ensure_token_valid(), self.hass.loop + self.data.session.async_ensure_token_valid(), self.hass.loop ).result() - self._spotify.set_auth(auth=self._session.token["access_token"]) + self.data.client.set_auth(auth=self.data.session.token["access_token"]) - current = self._spotify.current_playback() + current = self.data.client.current_playback() self._currently_playing = current or {} context = self._currently_playing.get("context") @@ -526,9 +378,11 @@ def update(self) -> None: ): self._playlist = None if context["type"] == MEDIA_TYPE_PLAYLIST: - self._playlist = self._spotify.playlist(current["context"]["uri"]) + self._playlist = self.data.client.playlist(current["context"]["uri"]) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if not self._scope_ok: @@ -539,9 +393,9 @@ async def async_browse_media(self, media_content_type=None, media_content_id=Non return await async_browse_media_internal( self.hass, - self._spotify, - self._session, - self._me, + self.data.client, + self.data.session, + self.data.current_user, media_content_type, media_content_id, ) @@ -557,273 +411,5 @@ async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() self.async_on_remove( - self._spotify_data[DATA_SPOTIFY_DEVICES].async_add_listener( - self._handle_devices_update - ) - ) - - -async def async_browse_media_internal( - hass, - spotify, - session, - current_user, - media_content_type, - media_content_id, - *, - can_play_artist=True, -): - """Browse spotify media.""" - if media_content_type in (None, f"{MEDIA_PLAYER_PREFIX}library"): - return await hass.async_add_executor_job( - partial(library_payload, can_play_artist=can_play_artist) - ) - - if not session.valid_token: - await session.async_ensure_token_valid() - await hass.async_add_executor_job( - spotify.set_auth, session.token["access_token"] - ) - - # Strip prefix - media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :] - - payload = { - "media_content_type": media_content_type, - "media_content_id": media_content_id, - } - response = await hass.async_add_executor_job( - partial( - build_item_response, - spotify, - current_user, - payload, - can_play_artist=can_play_artist, - ) - ) - if response is None: - raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") - return response - - -def build_item_response(spotify, user, payload, *, can_play_artist): # noqa: C901 - """Create response payload for the provided media query.""" - media_content_type = payload["media_content_type"] - media_content_id = payload["media_content_id"] - title = None - image = None - if media_content_type == BrowsableMedia.CURRENT_USER_PLAYLISTS: - media = spotify.current_user_playlists(limit=BROWSE_LIMIT) - items = media.get("items", []) - elif media_content_type == BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: - media = spotify.current_user_followed_artists(limit=BROWSE_LIMIT) - items = media.get("artists", {}).get("items", []) - elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: - media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT) - items = [item["album"] for item in media.get("items", [])] - elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_TRACKS: - media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT) - items = [item["track"] for item in media.get("items", [])] - elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_SHOWS: - media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT) - items = [item["show"] for item in media.get("items", [])] - elif media_content_type == BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: - media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) - items = [item["track"] for item in media.get("items", [])] - elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_ARTISTS: - media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) - items = media.get("items", []) - elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS: - media = spotify.current_user_top_tracks(limit=BROWSE_LIMIT) - items = media.get("items", []) - elif media_content_type == BrowsableMedia.FEATURED_PLAYLISTS: - media = spotify.featured_playlists(country=user["country"], limit=BROWSE_LIMIT) - items = media.get("playlists", {}).get("items", []) - elif media_content_type == BrowsableMedia.CATEGORIES: - media = spotify.categories(country=user["country"], limit=BROWSE_LIMIT) - items = media.get("categories", {}).get("items", []) - elif media_content_type == "category_playlists": - media = spotify.category_playlists( - category_id=media_content_id, - country=user["country"], - limit=BROWSE_LIMIT, + self.data.devices.async_add_listener(self._handle_devices_update) ) - category = spotify.category(media_content_id, country=user["country"]) - title = category.get("name") - image = fetch_image_url(category, key="icons") - items = media.get("playlists", {}).get("items", []) - elif media_content_type == BrowsableMedia.NEW_RELEASES: - media = spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT) - items = media.get("albums", {}).get("items", []) - elif media_content_type == MEDIA_TYPE_PLAYLIST: - media = spotify.playlist(media_content_id) - items = [item["track"] for item in media.get("tracks", {}).get("items", [])] - elif media_content_type == MEDIA_TYPE_ALBUM: - media = spotify.album(media_content_id) - items = media.get("tracks", {}).get("items", []) - elif media_content_type == MEDIA_TYPE_ARTIST: - media = spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT) - artist = spotify.artist(media_content_id) - title = artist.get("name") - image = fetch_image_url(artist) - items = media.get("items", []) - elif media_content_type == MEDIA_TYPE_SHOW: - media = spotify.show_episodes(media_content_id, limit=BROWSE_LIMIT) - show = spotify.show(media_content_id) - title = show.get("name") - image = fetch_image_url(show) - items = media.get("items", []) - else: - media = None - items = [] - - if media is None: - return None - - try: - media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] - except KeyError: - _LOGGER.debug("Unknown media type received: %s", media_content_type) - return None - - if media_content_type == BrowsableMedia.CATEGORIES: - media_item = BrowseMedia( - title=LIBRARY_MAP.get(media_content_id), - media_class=media_class["parent"], - children_media_class=media_class["children"], - media_content_id=media_content_id, - media_content_type=MEDIA_PLAYER_PREFIX + media_content_type, - can_play=False, - can_expand=True, - children=[], - ) - for item in items: - try: - item_id = item["id"] - except KeyError: - _LOGGER.debug("Missing ID for media item: %s", item) - continue - media_item.children.append( - BrowseMedia( - title=item.get("name"), - media_class=MEDIA_CLASS_PLAYLIST, - children_media_class=MEDIA_CLASS_TRACK, - media_content_id=item_id, - media_content_type=MEDIA_PLAYER_PREFIX + "category_playlists", - thumbnail=fetch_image_url(item, key="icons"), - can_play=False, - can_expand=True, - ) - ) - return media_item - - if title is None: - if "name" in media: - title = media.get("name") - else: - title = LIBRARY_MAP.get(payload["media_content_id"]) - - params = { - "title": title, - "media_class": media_class["parent"], - "children_media_class": media_class["children"], - "media_content_id": media_content_id, - "media_content_type": MEDIA_PLAYER_PREFIX + media_content_type, - "can_play": media_content_type in PLAYABLE_MEDIA_TYPES - and (media_content_type != MEDIA_TYPE_ARTIST or can_play_artist), - "children": [], - "can_expand": True, - } - for item in items: - try: - params["children"].append( - item_payload(item, can_play_artist=can_play_artist) - ) - except (MissingMediaInformation, UnknownMediaType): - continue - - if "images" in media: - params["thumbnail"] = fetch_image_url(media) - elif image: - params["thumbnail"] = image - - return BrowseMedia(**params) - - -def item_payload(item, *, can_play_artist): - """ - Create response payload for a single media item. - - Used by async_browse_media. - """ - try: - media_type = item["type"] - media_id = item["uri"] - except KeyError as err: - _LOGGER.debug("Missing type or URI for media item: %s", item) - raise MissingMediaInformation from err - - try: - media_class = CONTENT_TYPE_MEDIA_CLASS[media_type] - except KeyError as err: - _LOGGER.debug("Unknown media type received: %s", media_type) - raise UnknownMediaType from err - - can_expand = media_type not in [ - MEDIA_TYPE_TRACK, - MEDIA_TYPE_EPISODE, - ] - - payload = { - "title": item.get("name"), - "media_class": media_class["parent"], - "children_media_class": media_class["children"], - "media_content_id": media_id, - "media_content_type": MEDIA_PLAYER_PREFIX + media_type, - "can_play": media_type in PLAYABLE_MEDIA_TYPES - and (media_type != MEDIA_TYPE_ARTIST or can_play_artist), - "can_expand": can_expand, - } - - if "images" in item: - payload["thumbnail"] = fetch_image_url(item) - elif MEDIA_TYPE_ALBUM in item: - payload["thumbnail"] = fetch_image_url(item[MEDIA_TYPE_ALBUM]) - - return BrowseMedia(**payload) - - -def library_payload(*, can_play_artist): - """ - Create response payload to describe contents of a specific library. - - Used by async_browse_media. - """ - library_info = { - "title": "Media Library", - "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": "library", - "media_content_type": MEDIA_PLAYER_PREFIX + "library", - "can_play": False, - "can_expand": True, - "children": [], - } - - for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]: - library_info["children"].append( - item_payload( - {"name": item["name"], "type": item["type"], "uri": item["type"]}, - can_play_artist=can_play_artist, - ) - ) - response = BrowseMedia(**library_info) - response.children_media_class = MEDIA_CLASS_DIRECTORY - return response - - -def fetch_image_url(item, key="images"): - """Fetch image url.""" - try: - return item.get(key, [])[0].get("url") - except IndexError: - return None diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index f775e5df85d4c..caec5b8a288f5 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -11,8 +11,8 @@ }, "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, "create_entry": { "default": "Successfully authenticated with Spotify." } diff --git a/homeassistant/components/spotify/translations/cs.json b/homeassistant/components/spotify/translations/cs.json index 69cd1b1623ada..3b1ea17e15a0a 100644 --- a/homeassistant/components/spotify/translations/cs.json +++ b/homeassistant/components/spotify/translations/cs.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.", "missing_configuration": "Integrace Spotify 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})" + "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_account_mismatch": "Ov\u011b\u0159en\u00fd \u00fa\u010det Spotify neodpov\u00edd\u00e1 \u00fa\u010dtu, kter\u00fd vy\u017eaduje op\u011btovn\u00e9 ov\u011b\u0159en\u00ed." }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno pomoc\u00ed Spotify." @@ -17,5 +18,10 @@ "title": "Znovu ov\u011b\u0159it integraci" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API je dostupn\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 7c97846844d79..099a7ebcd3b7f 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -3,12 +3,16 @@ "abort": { "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify \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_account_mismatch": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 Spotify \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bf\u03c0\u03bf\u03af\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2, \u03b4\u03b5\u03bd \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd." }, "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify." }, "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, "reauth_confirm": { "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Spotify \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc: {account}", "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify" diff --git a/homeassistant/components/spotify/translations/pt-BR.json b/homeassistant/components/spotify/translations/pt-BR.json new file mode 100644 index 0000000000000..c49fbfabfa05d --- /dev/null +++ b/homeassistant/components/spotify/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "A integra\u00e7\u00e3o do Spotify n\u00e3o est\u00e1 configurada. 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_account_mismatch": "A conta Spotify autenticada com, n\u00e3o corresponde \u00e0 conta necess\u00e1ria para reautentica\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso no Spotify." + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Spotify precisa ser autenticada novamente com o Spotify para a conta: {account}", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint da API do Spotify acess\u00edvel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/sk.json b/homeassistant/components/spotify/translations/sk.json new file mode 100644 index 0000000000000..63ae49fe7273f --- /dev/null +++ b/homeassistant/components/spotify/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "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/spotify/util.py b/homeassistant/components/spotify/util.py new file mode 100644 index 0000000000000..7f7f682fb9e8e --- /dev/null +++ b/homeassistant/components/spotify/util.py @@ -0,0 +1,34 @@ +"""Utils for Spotify.""" +from __future__ import annotations + +from typing import Any + +import yarl + +from .const import MEDIA_PLAYER_PREFIX + + +def is_spotify_media_type(media_content_type: str) -> bool: + """Return whether the media_content_type is a valid Spotify media_id.""" + return media_content_type.startswith(MEDIA_PLAYER_PREFIX) + + +def resolve_spotify_media_type(media_content_type: str) -> str: + """Return actual spotify media_content_type.""" + return media_content_type[len(MEDIA_PLAYER_PREFIX) :] + + +def fetch_image_url(item: dict[str, Any], key="images") -> str | None: + """Fetch image url.""" + try: + return item.get(key, [])[0].get("url") + except IndexError: + return None + + +def spotify_uri_from_media_browser_url(media_content_id: str) -> str: + """Extract spotify URI from media browser URL.""" + if media_content_id and media_content_id.startswith(MEDIA_PLAYER_PREFIX): + parsed_url = yarl.URL(media_content_id) + media_content_id = parsed_url.name + return media_content_id diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index b9e3b9ce81d98..1c8514d0d2649 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -1,7 +1,7 @@ """Sensor from an SQL Query.""" from __future__ import annotations -import datetime +from datetime import date import decimal import logging import re @@ -11,11 +11,15 @@ import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant 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 _LOGGER = logging.getLogger(__name__) @@ -27,12 +31,12 @@ DB_URL_RE = re.compile("//.*:.*@") -def redact_credentials(data): +def redact_credentials(data: str) -> str: """Redact credentials from string data.""" return DB_URL_RE.sub("//****:****@", data) -def validate_sql_select(value): +def validate_sql_select(value: str) -> str: """Validate that value is a SQL SELECT query.""" if not value.lstrip().lower().startswith("select"): raise vol.Invalid("Only SELECT queries allowed") @@ -49,7 +53,7 @@ def validate_sql_select(value): } ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_QUERIES): [_QUERY_SCHEME], vol.Optional(CONF_DB_URL): cv.string} ) @@ -64,7 +68,7 @@ def setup_platform( if not (db_url := config.get(CONF_DB_URL)): db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) - sess = None + sess: scoped_session | None = None try: engine = sqlalchemy.create_engine(db_url) sessmaker = scoped_session(sessionmaker(bind=engine)) @@ -87,11 +91,11 @@ def setup_platform( queries = [] for query in config[CONF_QUERIES]: - name = query.get(CONF_NAME) - query_str = query.get(CONF_QUERY) - unit = query.get(CONF_UNIT_OF_MEASUREMENT) - value_template = query.get(CONF_VALUE_TEMPLATE) - column_name = query.get(CONF_COLUMN_NAME) + name: str = query[CONF_NAME] + query_str: str = query[CONF_QUERY] + unit: str | None = query.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = query.get(CONF_VALUE_TEMPLATE) + column_name: str = query[CONF_COLUMN_NAME] if value_template is not None: value_template.hass = hass @@ -115,60 +119,32 @@ def setup_platform( class SQLSensor(SensorEntity): """Representation of an SQL sensor.""" - def __init__(self, name, sessmaker, query, column, unit, value_template): + def __init__( + self, + name: str, + sessmaker: scoped_session, + query: str, + column: str, + unit: str | None, + value_template: Template | None, + ) -> None: """Initialize the SQL sensor.""" - self._name = name + self._attr_name = name self._query = query - self._unit_of_measurement = unit + self._attr_native_unit_of_measurement = unit self._template = value_template self._column_name = column self.sessionmaker = sessmaker - self._state = None - self._attributes = None - - @property - def name(self): - """Return the name of the query.""" - return self._name - - @property - def native_value(self): - """Return the query's current state.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - def update(self): + self._attr_extra_state_attributes = {} + + def update(self) -> None: """Retrieve sensor data from the query.""" data = None + self._attr_extra_state_attributes = {} + sess: scoped_session = self.sessionmaker() try: - sess = self.sessionmaker() result = sess.execute(self._query) - self._attributes = {} - - if not result.returns_rows or result.rowcount == 0: - _LOGGER.warning("%s returned no results", self._query) - self._state = None - return - - for res in result.mappings(): - _LOGGER.debug("result = %s", res.items()) - data = res[self._column_name] - for key, value in res.items(): - if isinstance(value, decimal.Decimal): - value = float(value) - if isinstance(value, datetime.date): - value = str(value) - self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error( "Error executing query %s: %s", @@ -176,12 +152,27 @@ def update(self): redact_credentials(str(err)), ) return - finally: - sess.close() + + _LOGGER.debug("Result %s, ResultMapping %s", result, result.mappings()) + + for res in result.mappings(): + _LOGGER.debug("result = %s", res.items()) + data = res[self._column_name] + for key, value in res.items(): + if isinstance(value, decimal.Decimal): + value = float(value) + if isinstance(value, date): + value = value.isoformat() + self._attr_extra_state_attributes[key] = value if data is not None and self._template is not None: - self._state = self._template.async_render_with_possible_json_value( - data, None + self._attr_native_value = ( + self._template.async_render_with_possible_json_value(data, None) ) else: - self._state = data + self._attr_native_value = data + + if not data: + _LOGGER.warning("%s returned no results", self._query) + + sess.close() diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index ec3089dc4bef8..f36917f1a01ab 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -11,5 +11,6 @@ "macaddress": "000420*" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysqueezebox"] } diff --git a/homeassistant/components/squeezebox/translations/el.json b/homeassistant/components/squeezebox/translations/el.json new file mode 100644 index 0000000000000..a8dcd409a81ea --- /dev/null +++ b/homeassistant/components/squeezebox/translations/el.json @@ -0,0 +1,31 @@ +{ + "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", + "no_server_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 LMS." + }, + "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", + "no_server_found": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{host}", + "step": { + "edit": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "\u0395\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/pt-BR.json b/homeassistant/components/squeezebox/translations/pt-BR.json new file mode 100644 index 0000000000000..28ca34f081863 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_server_found": "Nenhum servidor LMS encontrado." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_server_found": "N\u00e3o foi poss\u00edvel descobrir o servidor automaticamente.", + "unknown": "Erro inesperado" + }, + "flow_title": "{host}", + "step": { + "edit": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Editar informa\u00e7\u00f5es de conex\u00e3o" + }, + "user": { + "data": { + "host": "Nome do host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/sk.json b/homeassistant/components/squeezebox/translations/sk.json new file mode 100644 index 0000000000000..85b770fe2ed89 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "edit": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json index 73aac879a0027..f5d38f4d0738c 100644 --- a/homeassistant/components/srp_energy/manifest.json +++ b/homeassistant/components/srp_energy/manifest.json @@ -3,7 +3,8 @@ "name": "SRP Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/srp_energy", - "requirements": ["srpenergy==1.3.2"], + "requirements": ["srpenergy==1.3.6"], "codeowners": ["@briglx"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["srpenergy"] } diff --git a/homeassistant/components/srp_energy/translations/el.json b/homeassistant/components/srp_energy/translations/el.json index aaf69256d3622..4583eb47823c1 100644 --- a/homeassistant/components/srp_energy/translations/el.json +++ b/homeassistant/components/srp_energy/translations/el.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "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": { - "invalid_account": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 9\u03c8\u03ae\u03c6\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_account": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 9\u03c8\u03ae\u03c6\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\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": { + "user": { + "data": { + "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", + "is_tou": "\u0395\u03af\u03bd\u03b1\u03b9 \u03bf \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03c7\u03b5\u03b4\u03af\u03bf\u03c5", + "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" + } + } } - } + }, + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pt-BR.json b/homeassistant/components/srp_energy/translations/pt-BR.json new file mode 100644 index 0000000000000..b0dbfe9c6d04d --- /dev/null +++ b/homeassistant/components/srp_energy/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_account": "O ID da conta deve ser um n\u00famero de 9 d\u00edgitos", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "id": "ID da conta", + "is_tou": "Plano de tempo de uso", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/sk.json b/homeassistant/components/srp_energy/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/uk.json b/homeassistant/components/srp_energy/translations/uk.json index 5267aa2a5757f..144e40f15bb54 100644 --- a/homeassistant/components/srp_energy/translations/uk.json +++ b/homeassistant/components/srp_energy/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json index 87bf347795c98..adbf635100cb6 100644 --- a/homeassistant/components/srp_energy/translations/zh-Hant.json +++ b/homeassistant/components/srp_energy/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 44d972ca16abc..93512d08238df 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,10 +2,11 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.23.4"], + "requirements": ["async-upnp-client==0.23.5"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["async_upnp_client"] } diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index e487d8d63f0f7..d565b7aa69083 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/starline", "requirements": ["starline==0.1.5"], "codeowners": ["@anonym-tsk"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["starline"] } diff --git a/homeassistant/components/starline/translations/el.json b/homeassistant/components/starline/translations/el.json index 2716af6477b90..254bd2db7754f 100644 --- a/homeassistant/components/starline/translations/el.json +++ b/homeassistant/components/starline/translations/el.json @@ -29,6 +29,10 @@ "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, "auth_user": { + "data": { + "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": "Email \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd StarLine", "title": "\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } diff --git a/homeassistant/components/starline/translations/pt-BR.json b/homeassistant/components/starline/translations/pt-BR.json index 3b73793804a30..d9141c8b5cb19 100644 --- a/homeassistant/components/starline/translations/pt-BR.json +++ b/homeassistant/components/starline/translations/pt-BR.json @@ -1,17 +1,24 @@ { "config": { "error": { + "error_auth_app": "C\u00f3digo ou segredo do aplicativo incorreto", "error_auth_mfa": "C\u00f3digo incorreto", "error_auth_user": "Usu\u00e1rio ou senha incorretos" }, "step": { "auth_app": { + "data": { + "app_id": "ID do aplicativo", + "app_secret": "Segredo" + }, + "description": "ID do aplicativo e c\u00f3digo secreto de [conta de desenvolvedor StarLine](https://my.starline.ru/developer)", "title": "Credenciais do aplicativo" }, "auth_captcha": { "data": { "captcha_code": "C\u00f3digo da imagem" }, + "description": "{captcha_img}", "title": "Captcha" }, "auth_mfa": { @@ -25,7 +32,9 @@ "data": { "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "description": "Email e senha da conta StarLine", + "title": "Credenciais do usu\u00e1rio" } } } diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 8de4b4c24dc11..7f658c4409e8d 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": ["starlingbank==3.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["starlingbank"] } diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index dc87116914af7..042fc33060a37 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -194,7 +194,7 @@ def bytes_to_gb(value): :param value: The value in bytes to convert to GB. :return: Converted GB value """ - return float(value) * 10 ** -9 + return float(value) * 10**-9 @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index 5e4db0b6770f0..39c69e6052f63 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/statsd", "requirements": ["statsd==3.2.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["statsd"] } diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index ca5e4f1da53f6..47f645d7148c2 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["steam"] } diff --git a/homeassistant/components/steamist/config_flow.py b/homeassistant/components/steamist/config_flow.py index 00d87bf98371b..c0ec18157b6e4 100644 --- a/homeassistant/components/steamist/config_flow.py +++ b/homeassistant/components/steamist/config_flow.py @@ -48,10 +48,10 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes ) return await self._async_handle_discovery() - async def async_step_discovery( + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" + """Handle integration discovery.""" self._discovered_device = Device30303( ipaddress=discovery_info["ipaddress"], name=discovery_info["name"], diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index 2ecd2a1d68104..773e56d6612fa 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -125,7 +125,7 @@ def async_trigger_discovery( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ "ipaddress": device.ipaddress, "name": device.name, diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index e815b48233019..e3095ada47a33 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -8,9 +8,11 @@ "codeowners": ["@bdraco"], "iot_class": "local_polling", "dhcp": [ + {"registered_devices": true}, { "macaddress": "001E0C*", "hostname": "my[45]50*" } - ] + ], + "loggers": ["aiosteamist", "discovery30303"] } \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/bg.json b/homeassistant/components/steamist/translations/bg.json index dfe2f3cef7463..10f6abeb604e7 100644 --- a/homeassistant/components/steamist/translations/bg.json +++ b/homeassistant/components/steamist/translations/bg.json @@ -10,6 +10,9 @@ }, "flow_title": "{name} ({ipaddress})", "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({ipaddress})?" + }, "pick_device": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/steamist/translations/el.json b/homeassistant/components/steamist/translations/el.json index 2f022cfb50e71..0d18542305612 100644 --- a/homeassistant/components/steamist/translations/el.json +++ b/homeassistant/components/steamist/translations/el.json @@ -1,5 +1,16 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "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_steamist_device": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae steamist" + }, + "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" + }, "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { @@ -9,6 +20,12 @@ "data": { "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" } + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/steamist/translations/fr.json b/homeassistant/components/steamist/translations/fr.json index 95c4d883f02b2..f3c122a2f2ca1 100644 --- a/homeassistant/components/steamist/translations/fr.json +++ b/homeassistant/components/steamist/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "Impossible de se connecter", "no_devices_found": "Pas d'appareils trouv\u00e9 sur le r\u00e9seau", "not_steamist_device": "Pas un appareil \u00e0 vapeur" diff --git a/homeassistant/components/steamist/translations/he.json b/homeassistant/components/steamist/translations/he.json index ec05565b9a6e9..7b8528476e1ed 100644 --- a/homeassistant/components/steamist/translations/he.json +++ b/homeassistant/components/steamist/translations/he.json @@ -12,6 +12,11 @@ }, "flow_title": "{name} ({ipaddress})", "step": { + "pick_device": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/steamist/translations/id.json b/homeassistant/components/steamist/translations/id.json index 203ba41ae00ea..46fc072d9fe33 100644 --- a/homeassistant/components/steamist/translations/id.json +++ b/homeassistant/components/steamist/translations/id.json @@ -4,17 +4,28 @@ "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_steamist_device": "Bukan perangkat steamist" }, "error": { "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{name} ({ipaddress})", "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {name} ({ipaddress})?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } } } diff --git a/homeassistant/components/steamist/translations/nl.json b/homeassistant/components/steamist/translations/nl.json index ccd7379c346d8..345ec01dce348 100644 --- a/homeassistant/components/steamist/translations/nl.json +++ b/homeassistant/components/steamist/translations/nl.json @@ -16,10 +16,16 @@ "discovery_confirm": { "description": "Wilt u {name} ({ipaddress}) instellen?" }, + "pick_device": { + "data": { + "device": "Apparaat" + } + }, "user": { "data": { "host": "Host" - } + }, + "description": "Als u de host leeg laat, zal discovery worden gebruikt om apparaten te vinden." } } } diff --git a/homeassistant/components/steamist/translations/pt-BR.json b/homeassistant/components/steamist/translations/pt-BR.json new file mode 100644 index 0000000000000..b099c4b36f754 --- /dev/null +++ b/homeassistant/components/steamist/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_steamist_device": "N\u00e3o \u00e9 um dispositivo de vaporizador" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({ipaddress})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar {name} ( {ipaddress} )?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/sk.json b/homeassistant/components/steamist/translations/sk.json new file mode 100644 index 0000000000000..bee0999420fbf --- /dev/null +++ b/homeassistant/components/steamist/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 3f83c35ffa9d4..feb9657ef3163 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pystiebeleltron==0.0.1.dev2"], "dependencies": ["modbus"], "codeowners": ["@fucm"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pymodbus", "pystiebeleltron"] } diff --git a/homeassistant/components/stookalert/translations/el.json b/homeassistant/components/stookalert/translations/el.json new file mode 100644 index 0000000000000..4070ff0135707 --- /dev/null +++ b/homeassistant/components/stookalert/translations/el.json @@ -0,0 +1,14 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "province": "\u0395\u03c0\u03b1\u03c1\u03c7\u03af\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/pt-BR.json b/homeassistant/components/stookalert/translations/pt-BR.json new file mode 100644 index 0000000000000..5ad1d9f9a81fb --- /dev/null +++ b/homeassistant/components/stookalert/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "province": "Prov\u00edncia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 731fd41068695..157f20b5b37f7 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -120,10 +120,8 @@ def create_stream( def filter_libav_logging() -> None: """Filter libav logging to only log when the stream logger is at DEBUG.""" - stream_debug_enabled = logging.getLogger(__name__).isEnabledFor(logging.DEBUG) - def libav_filter(record: logging.LogRecord) -> bool: - return stream_debug_enabled + return logging.getLogger(__name__).isEnabledFor(logging.DEBUG) for logging_namespace in ( "libav.mp4", @@ -247,7 +245,7 @@ def add_provider( self, fmt: str, timeout: int = OUTPUT_IDLE_TIMEOUT ) -> StreamOutput: """Add provider output stream.""" - if not self._outputs.get(fmt): + if not (provider := self._outputs.get(fmt)): @callback def idle_callback() -> None: @@ -261,7 +259,7 @@ def idle_callback() -> None: self.hass, IdleTimer(self.hass, timeout, idle_callback) ) self._outputs[fmt] = provider - return self._outputs[fmt] + return provider def remove_provider(self, provider: StreamOutput) -> None: """Remove provider output stream.""" @@ -312,7 +310,9 @@ def start(self) -> None: def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" - self._logger.debug("Updating stream source %s", new_source) + self._logger.debug( + "Updating stream source %s", redact_credentials(str(new_source)) + ) self.source = new_source self._fast_restart_once = True self._thread_quit.set() @@ -340,7 +340,7 @@ def _run_worker(self) -> None: self._logger.error("Error from stream worker: %s", str(err)) stream_state.discontinuity() - if not self.keepalive or self._thread_quit.is_set(): + if not _should_retry() or self._thread_quit.is_set(): if self._fast_restart_once: # The stream source is updated, restart without any delay and reset the retry # backoff for the new url. @@ -361,7 +361,7 @@ def _run_worker(self) -> None: self._logger.debug( "Restarting stream worker in %d seconds: %s", wait_timeout, - self.source, + redact_credentials(str(self.source)), ) self._worker_finished() @@ -446,3 +446,8 @@ async def async_get_image( return await self._keyframe_converter.async_get_image( width=width, height=height ) + + +def _should_retry() -> bool: + """Return true if worker failures should be retried, for disabling during tests.""" + return True diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 91414dd96d951..8db6a23981809 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -23,7 +23,7 @@ from . import Stream -PROVIDERS = Registry() +PROVIDERS: Registry[str, type[StreamOutput]] = Registry() @attr.s(slots=True) diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index cb42752d966d6..20473b66f2aca 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "requirements": ["streamlabswater==1.0.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["streamlabswater"] } diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 2b7af28a91606..6e1151cdccbc1 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -3,7 +3,8 @@ "name": "Subaru", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/subaru", - "requirements": ["subarulink==0.3.12"], + "requirements": ["subarulink==0.4.2"], "codeowners": ["@G-Two"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["stdiomask", "subarulink"] } diff --git a/homeassistant/components/subaru/translations/el.json b/homeassistant/components/subaru/translations/el.json index f0d2561e4eed3..30ff37ccc1ee6 100644 --- a/homeassistant/components/subaru/translations/el.json +++ b/homeassistant/components/subaru/translations/el.json @@ -1,7 +1,14 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "error": { - "bad_pin_format": "\u03a4\u03bf PIN \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 4 \u03c8\u03b7\u03c6\u03af\u03b1" + "bad_pin_format": "\u03a4\u03bf PIN \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 4 \u03c8\u03b7\u03c6\u03af\u03b1", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_pin": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf PIN", + "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": { "pin": { diff --git a/homeassistant/components/subaru/translations/nb.json b/homeassistant/components/subaru/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/subaru/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/pt-BR.json b/homeassistant/components/subaru/translations/pt-BR.json new file mode 100644 index 0000000000000..e88a9e883c048 --- /dev/null +++ b/homeassistant/components/subaru/translations/pt-BR.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falha ao conectar" + }, + "error": { + "bad_pin_format": "O PIN deve ter 4 d\u00edgitos", + "cannot_connect": "Falha ao conectar", + "incorrect_pin": "PIN incorreto", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Por favor, digite seu PIN MySubaru\n NOTA: Todos os ve\u00edculos em conta devem ter o mesmo PIN", + "title": "Configura\u00e7\u00e3o do Subaru Starlink" + }, + "user": { + "data": { + "country": "Selecione o pa\u00eds", + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Por favor, insira suas credenciais MySubaru\n NOTA: A configura\u00e7\u00e3o inicial pode demorar at\u00e9 30 segundos", + "title": "Configura\u00e7\u00e3o do Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Habilitar as pesquisas de ve\u00edculos" + }, + "description": "Quando ativado, a pesquisa de ve\u00edculos enviar\u00e1 um comando remoto para seu ve\u00edculo a cada 2 horas para obter novos dados do sensor. Sem a pesquisa de ve\u00edculos, os novos dados do sensor s\u00f3 s\u00e3o recebidos quando o ve\u00edculo enviar\u00e1 automaticamente os dados (normalmente ap\u00f3s o desligamento do motor).", + "title": "Op\u00e7\u00f5es do Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/sk.json b/homeassistant/components/subaru/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/subaru/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 20c8ba1dfed76..ddda3caf2ffe6 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/suez_water", "codeowners": ["@ooii"], "requirements": ["pysuez==0.1.19"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pysuez", "regex"] } diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index 6420e39538e9e..789ac76512cb5 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/supla", "requirements": ["asyncpysupla==0.0.5"], "codeowners": ["@mwegrzynek"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["asyncpysupla"] } diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 13def08280a6d..8675099c530e9 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -10,5 +10,6 @@ "surepy==0.7.2" ], "iot_class": "cloud_polling", - "config_flow": true + "config_flow": true, + "loggers": ["rich", "surepy"] } \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/el.json b/homeassistant/components/surepetcare/translations/el.json new file mode 100644 index 0000000000000..cdc7ae85736f1 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/el.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/nb.json b/homeassistant/components/surepetcare/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/pt-BR.json b/homeassistant/components/surepetcare/translations/pt-BR.json index c41610abb323c..d86aef5d51d73 100644 --- a/homeassistant/components/surepetcare/translations/pt-BR.json +++ b/homeassistant/components/surepetcare/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "A conta j\u00e1 foi configurada" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/surepetcare/translations/sk.json b/homeassistant/components/surepetcare/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index 7d7280ecc5fa8..a0400cb543b4e 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", "requirements": ["swisshydrodata==0.1.0"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["swisshydrodata"] } diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index 1a4a90031ba72..1fe5316c78a76 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": ["python_opendata_transport==0.3.0"], "codeowners": ["@fabaff"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["opendata_transport"] } diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index 413ab93839a60..95a60ab55eaa0 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { + "changed_states": "{entity_name} activado o desactivado", "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index 8758610a4a228..e2abc37090953 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { + "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} \u00e9teint", "turned_on": "{entity_name} allum\u00e9" diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json index 070d272aa4344..ca34137871421 100644 --- a/homeassistant/components/switch/translations/id.json +++ b/homeassistant/components/switch/translations/id.json @@ -10,6 +10,8 @@ "is_on": "{entity_name} nyala" }, "trigger_type": { + "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", + "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/switch/translations/nl.json b/homeassistant/components/switch/translations/nl.json index dbc4dc19f3752..71d3b0f6b8ef8 100644 --- a/homeassistant/components/switch/translations/nl.json +++ b/homeassistant/components/switch/translations/nl.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { + "changed_states": "{entity_name} in- of uitgeschakeld", "toggled": "{entity_name} in-of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" diff --git a/homeassistant/components/switch/translations/pl.json b/homeassistant/components/switch/translations/pl.json index 4fb5542f510d6..9132a6c6b9eb0 100644 --- a/homeassistant/components/switch/translations/pl.json +++ b/homeassistant/components/switch/translations/pl.json @@ -10,6 +10,7 @@ "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { + "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" diff --git a/homeassistant/components/switch/translations/pt-BR.json b/homeassistant/components/switch/translations/pt-BR.json index a3dcc96c80b3f..cb73ce3c5cf68 100644 --- a/homeassistant/components/switch/translations/pt-BR.json +++ b/homeassistant/components/switch/translations/pt-BR.json @@ -1,4 +1,21 @@ { + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "changed_states": "{entity_name} ligado ou desligado", + "toggled": "{entity_name} ligado ou desligado", + "turned_off": "{entity_name} desligado", + "turned_on": "{entity_name} ligado" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 59415d31c1edb..7a16225dcbbf2 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,8 +2,9 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.13.2"], + "requirements": ["PySwitchbot==0.13.3"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["switchbot"] } diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json new file mode 100644 index 0000000000000..fe3b7448ac86f --- /dev/null +++ b/homeassistant/components/switchbot/translations/el.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "switchbot_unsupported_type": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 Switchbot.", + "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" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "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" + } + } + }, + "options": { + "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)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json new file mode 100644 index 0000000000000..bf9cb746dcbd3 --- /dev/null +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado foi encontrado.", + "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Endere\u00e7o MAC do dispositivo", + "name": "Nome", + "password": "Senha" + }, + "title": "Configurar dispositivo Switchbot" + } + } + }, + "options": { + "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)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/switchbot/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/config_flow.py b/homeassistant/components/switcher_kis/config_flow.py index 3c7587152052e..d196bae8568ef 100644 --- a/homeassistant/components/switcher_kis/config_flow.py +++ b/homeassistant/components/switcher_kis/config_flow.py @@ -5,7 +5,6 @@ from homeassistant import config_entries from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import ConfigType from .const import DATA_DISCOVERY, DOMAIN from .utils import async_discover_devices @@ -14,7 +13,7 @@ class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Switcher config flow.""" - async def async_step_import(self, import_config: ConfigType) -> FlowResult: + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Handle a flow initiated by import.""" if self._async_current_entries(True): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index c8ed7ceefdcdb..9ebf83b4acd59 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aioswitcher==2.0.6"], "quality_scale": "platinum", "iot_class": "local_push", - "config_flow": true + "config_flow": true, + "loggers": ["aioswitcher"] } diff --git a/homeassistant/components/switcher_kis/translations/el.json b/homeassistant/components/switcher_kis/translations/el.json new file mode 100644 index 0000000000000..a13912159002b --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/pt-BR.json b/homeassistant/components/switcher_kis/translations/pt-BR.json new file mode 100644 index 0000000000000..1778d39a7d082 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/zh-Hant.json b/homeassistant/components/switcher_kis/translations/zh-Hant.json index 90c98e491dfea..cfd20d603cba1 100644 --- a/homeassistant/components/switcher_kis/translations/zh-Hant.json +++ b/homeassistant/components/switcher_kis/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index 042ccd93091fe..c4a263aca19a3 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/switchmate", "requirements": ["pySwitchmate==0.4.6"], "codeowners": ["@danielhiversen"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["switchmate"] } diff --git a/homeassistant/components/syncthing/manifest.json b/homeassistant/components/syncthing/manifest.json index cd779e1657b78..9d2897abf66b8 100644 --- a/homeassistant/components/syncthing/manifest.json +++ b/homeassistant/components/syncthing/manifest.json @@ -8,5 +8,6 @@ "@zhulik" ], "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aiosyncthing"] } diff --git a/homeassistant/components/syncthing/translations/cs.json b/homeassistant/components/syncthing/translations/cs.json new file mode 100644 index 0000000000000..a679dc35fe3ae --- /dev/null +++ b/homeassistant/components/syncthing/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/el.json b/homeassistant/components/syncthing/translations/el.json index d71bea91ddb3a..7d8c1e635dfb4 100644 --- a/homeassistant/components/syncthing/translations/el.json +++ b/homeassistant/components/syncthing/translations/el.json @@ -1,11 +1,22 @@ { "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_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": { "data": { - "title": "\u0395\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Syncthing" + "title": "\u0395\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Syncthing", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "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" } } } - } + }, + "title": "Syncthing" } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/pt-BR.json b/homeassistant/components/syncthing/translations/pt-BR.json new file mode 100644 index 0000000000000..08f66569d933d --- /dev/null +++ b/homeassistant/components/syncthing/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "title": "Configurar integra\u00e7\u00e3o do Syncthing", + "token": "Token", + "url": "URL", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + }, + "title": "Sincroniza\u00e7\u00e3o" +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/sk.json b/homeassistant/components/syncthing/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/syncthing/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 37b7ed311cb58..4536e703ce9f6 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -11,5 +11,6 @@ } ], "codeowners": ["@nielstron"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pysyncthru"] } diff --git a/homeassistant/components/syncthru/translations/el.json b/homeassistant/components/syncthru/translations/el.json new file mode 100644 index 0000000000000..e20c18577e5ae --- /dev/null +++ b/homeassistant/components/syncthru/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" + }, + "error": { + "invalid_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "syncthru_not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 SyncThru", + "unknown_state": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7, \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03b9\u03c3\u03c4\u03bf\u03cd" + } + }, + "user": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03b9\u03c3\u03c4\u03bf\u03cd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/pt-BR.json b/homeassistant/components/syncthru/translations/pt-BR.json new file mode 100644 index 0000000000000..0872de05be9a0 --- /dev/null +++ b/homeassistant/components/syncthru/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_url": "URL inv\u00e1lida", + "syncthru_not_supported": "O dispositivo n\u00e3o suporta SyncThru", + "unknown_state": "Estado da impressora desconhecido, verifique o URL e a conectividade de rede" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "Nome", + "url": "URL da interface da Web" + } + }, + "user": { + "data": { + "name": "Nome", + "url": "URL da interface da Web" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/sk.json b/homeassistant/components/syncthru/translations/sk.json new file mode 100644 index 0000000000000..3d28cc36f74b5 --- /dev/null +++ b/homeassistant/components/syncthru/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "name": "N\u00e1zov" + } + }, + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 55e83dc52bf5f..37100f1a75973 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -382,6 +382,5 @@ class SynologyDSMSwitchEntityDescription( key="home_mode", name="Home Mode", icon="mdi:home-account", - entity_category=EntityCategory.CONFIG, ), ) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 124679c351677..39eb11903888d 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -11,5 +11,6 @@ "deviceType": "urn:schemas-upnp-org:device:Basic:1" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["synology_dsm"] } diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 305e6dda4e7c1..18014de8c7a1d 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -105,7 +105,7 @@ def native_value(self) -> Any | None: # Data (RAM) if self.native_unit_of_measurement == DATA_MEGABYTES: - return round(attr / 1024.0 ** 2, 1) + return round(attr / 1024.0**2, 1) # Network if self.native_unit_of_measurement == DATA_RATE_KILOBYTES_PER_SECOND: @@ -147,7 +147,7 @@ def native_value(self) -> Any | None: # Data (disk space) if self.native_unit_of_measurement == DATA_TERABYTES: - return round(attr / 1024.0 ** 4, 2) + return round(attr / 1024.0**4, 2) return attr diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py index a7a336e0c1b07..130ad110b4625 100644 --- a/homeassistant/components/synology_dsm/service.py +++ b/homeassistant/components/synology_dsm/service.py @@ -45,8 +45,7 @@ async def service_handler(call: ServiceCall) -> None: return if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]: - dsm_device = hass.data[DOMAIN].get(serial) - if not dsm_device: + if not (dsm_device := hass.data[DOMAIN].get(serial)): LOGGER.error("DSM with specified serial %s not found", serial) return LOGGER.debug("%s DSM with serial %s", call.service, serial) diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index f77c82130a41f..acf35c4c2a4b4 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -53,7 +53,8 @@ "step": { "init": { "data": { - "scan_interval": "\u041c\u0438\u043d\u0443\u0442\u0438 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0438\u044f\u0442\u0430" + "scan_interval": "\u041c\u0438\u043d\u0443\u0442\u0438 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0438\u044f\u0442\u0430", + "snap_profile_type": "\u041d\u0438\u0432\u043e \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u043d\u0430 \u0441\u043d\u0438\u043c\u043a\u0438\u0442\u0435 \u043e\u0442 \u043a\u0430\u043c\u0435\u0440\u0430\u0442\u0430 (0:\u0432\u0438\u0441\u043e\u043a\u043e 1:\u0441\u0440\u0435\u0434\u043d\u043e 2:\u043d\u0438\u0441\u043a\u043e)" } } } diff --git a/homeassistant/components/synology_dsm/translations/el.json b/homeassistant/components/synology_dsm/translations/el.json index 18cd08b5507ac..1cf10eb6175a1 100644 --- a/homeassistant/components/synology_dsm/translations/el.json +++ b/homeassistant/components/synology_dsm/translations/el.json @@ -1,12 +1,18 @@ { "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", + "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", "reconfigure_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \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", + "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", "missing_data": "\u039b\u03b5\u03af\u03c0\u03bf\u03c5\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1: \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03ae \u03ac\u03bb\u03bb\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", - "otp_failed": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03b2\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bd\u03ad\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + "otp_failed": "\u039f \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03b2\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bd\u03ad\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { @@ -15,10 +21,40 @@ "title": "Synology DSM: \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03b2\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd" }, "link": { + "data": { + "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" + }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", "title": "Synology DSM" }, + "reauth": { + "data": { + "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": "\u0391\u03b9\u03c4\u03af\u03b1: {details}", + "title": "Synology DSM \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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "title": "Synology DSM \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": { + "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", + "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" + }, "title": "Synology DSM" } } diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 503db0246034b..3476676682853 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Minutes entre les scans", + "snap_profile_type": "Niveau de qualit\u00e9 des instantan\u00e9s de la cam\u00e9ra (0 : \u00e9lev\u00e9 1 : moyen 2 : faible)", "timeout": "D\u00e9lai d'expiration (secondes)" } } diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index 8169a6be4bf6e..7f0242fedd03b 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Interval pemindaian dalam menit", + "snap_profile_type": "Tingkat kualitas snapshot kamera (0:tinggi, 1:sedang, 2:rendah)", "timeout": "Tenggang waktu (detik)" } } diff --git a/homeassistant/components/synology_dsm/translations/nb.json b/homeassistant/components/synology_dsm/translations/nb.json new file mode 100644 index 0000000000000..3a397e1f7d35e --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "username": "Brukernavn" + } + }, + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 8740308faf09e..801c1d7fe8286 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Minuten tussen scans", + "snap_profile_type": "Kwaliteitsniveau van camera-snapshots (0:hoog 1:gemiddeld 2:laag)", "timeout": "Time-out (seconden)" } } diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 121ffb0c6bfa6..41f56c135aab6 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Minutter mellom skanninger", + "snap_profile_type": "Kvalitetsniv\u00e5et p\u00e5 kameraets \u00f8yeblikksbilder (0:h\u00f8y 1:middels 2:lav)", "timeout": "Tidsavbrudd (sekunder)" } } diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index 06a21cfdf18f1..9e285f65f843b 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji [min]", + "snap_profile_type": "Poziom jako\u015bci zdj\u0119\u0107 z kamery (0:wysoka 1:\u015brednia 2:niska)", "timeout": "Limit czasu (sekundy)" } } diff --git a/homeassistant/components/synology_dsm/translations/pt-BR.json b/homeassistant/components/synology_dsm/translations/pt-BR.json index e633eb9128d40..ddde3e1e29db8 100644 --- a/homeassistant/components/synology_dsm/translations/pt-BR.json +++ b/homeassistant/components/synology_dsm/translations/pt-BR.json @@ -1,22 +1,61 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reconfigure_successful": "A reconfigura\u00e7\u00e3o foi bem-sucedida" + }, "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "missing_data": "Dados ausentes: tente novamente mais tarde ou outra configura\u00e7\u00e3o", "otp_failed": "Falha na autentica\u00e7\u00e3o em duas etapas, tente novamente com um novo c\u00f3digo", - "unknown": "Erro desconhecido: verifique os logs para obter mais detalhes" + "unknown": "Erro inesperado" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { "otp_code": "C\u00f3digo" - } + }, + "title": "Synology DSM: autentica\u00e7\u00e3o em duas etapas" }, "link": { "data": { - "ssl": "Use SSL/TLS para conectar-se ao seu NAS" + "password": "Senha", + "port": "Porta", + "ssl": "Usar um certificado SSL", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" }, "description": "Voc\u00ea quer configurar o {name} ({host})?", "title": "Synology DSM" + }, + "reauth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Motivo: {details}", + "title": "Synology DSM Reautenticar Integra\u00e7\u00e3o" + }, + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "title": "Synology DSM Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "ssl": "Usar um certificado SSL", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + }, + "title": "Synology DSM" } } }, @@ -24,7 +63,9 @@ "step": { "init": { "data": { - "scan_interval": "Minutos entre os escaneamentos" + "scan_interval": "Minutos entre os escaneamentos", + "snap_profile_type": "N\u00edvel de qualidade dos instant\u00e2neos da c\u00e2mera (0: alto 1: m\u00e9dio 2: baixo)", + "timeout": "Tempo limite (segundos)" } } } diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json new file mode 100644 index 0000000000000..e9c37059842de --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "link": { + "data": { + "port": "Port" + } + }, + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 3a0f66f1245de..a6f5c496f2233 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -27,6 +27,7 @@ "init": { "data": { "scan_interval": "Minuter mellan skanningar", + "snap_profile_type": "Kvalitetsniv\u00e5 p\u00e5 kamerabilder (0:h\u00f6g 1:medel 2:l\u00e5g)", "timeout": "Timeout (sekunder)" } } diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index b4d96f6f9b1bc..5ee3e114f1fdd 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": ["synology-srm==0.2.0"], "codeowners": ["@aerialls"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["synology_srm"] } diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 7610e76b7bbd6..896309f2593a5 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -63,16 +63,20 @@ async def _listen_for_events(self) -> None: await self.bridge.async_send_event( "get-data", [ - "battery", - "cpu", - "display", - "filesystem", - "graphics", - "memory", - "network", - "os", - "processes", - "system", + {"service": "battery", "method": "findAll", "observe": True}, + {"service": "cpu", "method": "findAll", "observe": True}, + {"service": "display", "method": "findAll", "observe": True}, + {"service": "filesystem", "method": "findSizes", "observe": True}, + {"service": "graphics", "method": "findAll", "observe": True}, + {"service": "memory", "method": "findAll", "observe": True}, + {"service": "network", "method": "findAll", "observe": True}, + {"service": "os", "method": "findAll", "observe": False}, + { + "service": "processes", + "method": "findCurrentLoad", + "observe": True, + }, + {"service": "system", "method": "findAll", "observe": False}, ], ) await self.bridge.listen_for_events(callback=self.async_handle_event) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index cd4ee5a51a18e..8fba9dd30cfd9 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,10 +3,11 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==2.2.3"], + "requirements": ["systembridge==2.3.1"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], "quality_scale": "silver", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["systembridge"] } diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 9a4f0cc0aa84a..c4969e2c14cf4 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -104,7 +104,7 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", - value=lambda bridge: round(bridge.memory.free / 1000 ** 3, 2), + value=lambda bridge: round(bridge.memory.free / 1000**3, 2), ), SystemBridgeSensorEntityDescription( key="memory_used_percentage", @@ -121,7 +121,7 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", - value=lambda bridge: round(bridge.memory.used / 1000 ** 3, 2), + value=lambda bridge: round(bridge.memory.used / 1000**3, 2), ), SystemBridgeSensorEntityDescription( key="os", @@ -324,7 +324,7 @@ async def async_setup_entry( native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge, i=index: round( - bridge.graphics.controllers[i].memoryFree / 10 ** 3, 2 + bridge.graphics.controllers[i].memoryFree / 10**3, 2 ), ), ), @@ -356,7 +356,7 @@ async def async_setup_entry( native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", value=lambda bridge, i=index: round( - bridge.graphics.controllers[i].memoryUsed / 10 ** 3, 2 + bridge.graphics.controllers[i].memoryUsed / 10**3, 2 ), ), ), diff --git a/homeassistant/components/system_bridge/translations/cs.json b/homeassistant/components/system_bridge/translations/cs.json new file mode 100644 index 0000000000000..372a54786bd06 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/el.json b/homeassistant/components/system_bridge/translations/el.json index 327da66531489..5576af325de9d 100644 --- a/homeassistant/components/system_bridge/translations/el.json +++ b/homeassistant/components/system_bridge/translations/el.json @@ -1,11 +1,29 @@ { "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", + "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", + "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" + }, "flow_title": "{name}", "step": { "authenticate": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bf\u03c1\u03af\u03c3\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {name}." }, "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2." } } diff --git a/homeassistant/components/system_bridge/translations/pt-BR.json b/homeassistant/components/system_bridge/translations/pt-BR.json new file mode 100644 index 0000000000000..ed1bcd01ded0c --- /dev/null +++ b/homeassistant/components/system_bridge/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "Chave da API" + }, + "description": "Insira a chave de API que voc\u00ea definiu em sua configura\u00e7\u00e3o para {name} ." + }, + "user": { + "data": { + "api_key": "Chave da API", + "host": "Nome do host", + "port": "Porta" + }, + "description": "Por favor, insira os detalhes da sua conex\u00e3o." + } + } + }, + "title": "Ponte do sistema" +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/sk.json b/homeassistant/components/system_bridge/translations/sk.json new file mode 100644 index 0000000000000..276eac51dd460 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "authenticate": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_health/translations/it.json b/homeassistant/components/system_health/translations/it.json index b270bd0f2e91d..a7a583f3f4545 100644 --- a/homeassistant/components/system_health/translations/it.json +++ b/homeassistant/components/system_health/translations/it.json @@ -1,3 +1,3 @@ { - "title": "Integrit\u00e0 del Sistema" + "title": "Integrit\u00e0 del sistema" } \ No newline at end of file diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index cc79ed12e1e13..e8a63c9c40a36 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": ["psutil==5.8.0"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["psutil"] } diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 79e5e3f9feae5..7a57c06ef5813 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -4,7 +4,7 @@ import asyncio from dataclasses import dataclass from datetime import datetime, timedelta -from functools import lru_cache +from functools import cache import logging import os import socket @@ -51,7 +51,7 @@ CONF_ARG = "arg" -if sys.maxsize > 2 ** 32: +if sys.maxsize > 2**32: CPU_ICON = "mdi:cpu-64-bit" else: CPU_ICON = "mdi:cpu-32-bit" @@ -473,22 +473,22 @@ def _update( # noqa: C901 if type_ == "disk_use_percent": state = _disk_usage(data.argument).percent elif type_ == "disk_use": - state = round(_disk_usage(data.argument).used / 1024 ** 3, 1) + state = round(_disk_usage(data.argument).used / 1024**3, 1) elif type_ == "disk_free": - state = round(_disk_usage(data.argument).free / 1024 ** 3, 1) + state = round(_disk_usage(data.argument).free / 1024**3, 1) elif type_ == "memory_use_percent": state = _virtual_memory().percent elif type_ == "memory_use": virtual_memory = _virtual_memory() - state = round((virtual_memory.total - virtual_memory.available) / 1024 ** 2, 1) + state = round((virtual_memory.total - virtual_memory.available) / 1024**2, 1) elif type_ == "memory_free": - state = round(_virtual_memory().available / 1024 ** 2, 1) + state = round(_virtual_memory().available / 1024**2, 1) elif type_ == "swap_use_percent": state = _swap_memory().percent elif type_ == "swap_use": - state = round(_swap_memory().used / 1024 ** 2, 1) + state = round(_swap_memory().used / 1024**2, 1) elif type_ == "swap_free": - state = round(_swap_memory().free / 1024 ** 2, 1) + state = round(_swap_memory().free / 1024**2, 1) elif type_ == "processor_use": state = round(psutil.cpu_percent(interval=None)) elif type_ == "processor_temperature": @@ -510,7 +510,7 @@ def _update( # noqa: C901 counters = _net_io_counters() if data.argument in counters: counter = counters[data.argument][IO_COUNTER[type_]] - state = round(counter / 1024 ** 2, 1) + state = round(counter / 1024**2, 1) else: state = None elif type_ in ("packets_out", "packets_in"): @@ -527,7 +527,7 @@ def _update( # noqa: C901 if data.value and data.value < counter: state = round( (counter - data.value) - / 1000 ** 2 + / 1000**2 / (now - (data.update_time or now)).total_seconds(), 3, ) @@ -561,34 +561,32 @@ def _update( # noqa: C901 return state, value, update_time -# When we drop python 3.8 support these can be switched to -# @cache https://docs.python.org/3.9/library/functools.html#functools.cache -@lru_cache(maxsize=None) +@cache def _disk_usage(path: str) -> Any: return psutil.disk_usage(path) -@lru_cache(maxsize=None) +@cache def _swap_memory() -> Any: return psutil.swap_memory() -@lru_cache(maxsize=None) +@cache def _virtual_memory() -> Any: return psutil.virtual_memory() -@lru_cache(maxsize=None) +@cache def _net_io_counters() -> Any: return psutil.net_io_counters(pernic=True) -@lru_cache(maxsize=None) +@cache def _net_if_addrs() -> Any: return psutil.net_if_addrs() -@lru_cache(maxsize=None) +@cache def _getloadavg() -> tuple[float, float, float]: return os.getloadavg() diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index f89ad5feedc30..42fc806ab54fb 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -103,7 +103,7 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi hass.config_entries.async_update_entry(entry, options=options) -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 50561042ea765..529b4bcfb970d 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -13,5 +13,6 @@ "hostname": "tado*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["PyTado"] } diff --git a/homeassistant/components/tado/translations/el.json b/homeassistant/components/tado/translations/el.json index 319a399445947..7fca0f12f444e 100644 --- a/homeassistant/components/tado/translations/el.json +++ b/homeassistant/components/tado/translations/el.json @@ -1,7 +1,20 @@ { "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", + "no_homes": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c0\u03af\u03c4\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc tado.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "user": { + "data": { + "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" + }, "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Tado" } } diff --git a/homeassistant/components/tado/translations/pt-BR.json b/homeassistant/components/tado/translations/pt-BR.json index af32cb3c3a617..68cf24fe5dfef 100644 --- a/homeassistant/components/tado/translations/pt-BR.json +++ b/homeassistant/components/tado/translations/pt-BR.json @@ -1,16 +1,20 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "no_homes": "N\u00e3o h\u00e1 casas vinculadas a esta conta Tado.", "unknown": "Erro inesperado" }, "step": { "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, "title": "Conecte-se \u00e0 sua conta Tado" } } @@ -21,6 +25,7 @@ "data": { "fallback": "Ative o modo de fallback." }, + "description": "O modo Fallback mudar\u00e1 para Smart Schedule na pr\u00f3xima troca de agendamento ap\u00f3s ajustar manualmente uma zona.", "title": "Ajuste as op\u00e7\u00f5es do Tado." } } diff --git a/homeassistant/components/tado/translations/sk.json b/homeassistant/components/tado/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/tado/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/el.json b/homeassistant/components/tag/translations/el.json new file mode 100644 index 0000000000000..192b190cf3745 --- /dev/null +++ b/homeassistant/components/tag/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0395\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1" +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/cs.json b/homeassistant/components/tailscale/translations/cs.json new file mode 100644 index 0000000000000..3bfe94e68ddda --- /dev/null +++ b/homeassistant/components/tailscale/translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/el.json b/homeassistant/components/tailscale/translations/el.json new file mode 100644 index 0000000000000..0c08e11eee8cf --- /dev/null +++ b/homeassistant/components/tailscale/translations/el.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "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", + "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_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a4\u03b1 \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9\u03b1 API \u03c4\u03b7\u03c2 Tailscale \u03b9\u03c3\u03c7\u03cd\u03bf\u03c5\u03bd \u03b3\u03b9\u03b1 90 \u03b7\u03bc\u03ad\u03c1\u03b5\u03c2. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c4\u03b7\u03c2 Tailscale \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "tailnet": "Tailnet" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7\u03bd Tailscale \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03c4\u03bf https://login.tailscale.com/admin/settings/authkeys.\n\n\u03a4\u03bf Tailnet \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c3\u03b1\u03c2 Tailscale. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ae \u03b3\u03c9\u03bd\u03af\u03b1 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Tailscale (\u03b4\u03af\u03c0\u03bb\u03b1 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03bf\u03c5 Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/nb.json b/homeassistant/components/tailscale/translations/nb.json new file mode 100644 index 0000000000000..7fa228d894a0d --- /dev/null +++ b/homeassistant/components/tailscale/translations/nb.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig autentisering" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/pt-BR.json b/homeassistant/components/tailscale/translations/pt-BR.json new file mode 100644 index 0000000000000..ddbdda9d5a6d3 --- /dev/null +++ b/homeassistant/components/tailscale/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "description": "Os tokens da API Tailscale s\u00e3o v\u00e1lidos por 90 dias. Voc\u00ea pode criar uma nova chave de API Tailscale em https://login.tailscale.com/admin/settings/authkeys." + }, + "user": { + "data": { + "api_key": "Chave da API", + "tailnet": "Tailnet" + }, + "description": "Para autenticar com o Tailscale, voc\u00ea precisar\u00e1 criar uma chave de API em https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet \u00e9 o nome da sua rede Tailscale. Voc\u00ea pode encontr\u00e1-lo no canto superior esquerdo no painel de administra\u00e7\u00e3o do Tailscale (ao lado do logotipo do Tailscale)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/sk.json b/homeassistant/components/tailscale/translations/sk.json new file mode 100644 index 0000000000000..4eba3bdc8bb9d --- /dev/null +++ b/homeassistant/components/tailscale/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index 62a667af5b14e..a9ebcb546b518 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tank_utility", "requirements": ["tank_utility==1.4.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["tank_utility"] } diff --git a/homeassistant/components/tankerkoenig/manifest.json b/homeassistant/components/tankerkoenig/manifest.json index d49ee6a125527..d3ad7fbe2e1bd 100644 --- a/homeassistant/components/tankerkoenig/manifest.json +++ b/homeassistant/components/tankerkoenig/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tankerkoenig", "requirements": ["pytankerkoenig==0.0.6"], "codeowners": ["@guillempages"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pytankerkoenig"] } diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index f8c4dff154522..6904f90a40202 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": ["tapsaff==0.2.1"], "codeowners": ["@bazwilliams"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tapsaff"] } diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index f8dcd4035df39..44dd248917780 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -19,12 +19,14 @@ from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt.subscription import ( + async_prepare_subscribe_topics, async_subscribe_topics, async_unsubscribe_topics, ) from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, EVENT_DEVICE_REGISTRY_UPDATED, @@ -62,14 +64,16 @@ async def _subscribe_topics(sub_state: dict | None, topics: dict) -> dict: for topic in topics.values(): if "msg_callback" in topic and "event_loop_safe" in topic: topic["msg_callback"] = callback(topic["msg_callback"]) - return await async_subscribe_topics(hass, sub_state, topics) + sub_state = async_prepare_subscribe_topics(hass, sub_state, topics) + await async_subscribe_topics(hass, sub_state) + return sub_state async def _unsubscribe_topics(sub_state: dict | None) -> dict: - return await async_unsubscribe_topics(hass, sub_state) + return async_unsubscribe_topics(hass, sub_state) tasmota_mqtt = TasmotaMQTTClient(_publish, _subscribe_topics, _unsubscribe_topics) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) async def async_discover_device(config: TasmotaDeviceConfig, mac: str) -> None: """Discover and add a Tasmota device.""" @@ -77,25 +81,40 @@ async def async_discover_device(config: TasmotaDeviceConfig, mac: str) -> None: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def async_device_removed(event: Event) -> None: + async def async_device_updated(event: Event) -> None: """Handle the removal of a device.""" - device_registry = await hass.helpers.device_registry.async_get_registry() - if event.data["action"] != "remove": + device_registry = dr.async_get(hass) + device_id = event.data["device_id"] + if event.data["action"] not in ("remove", "update"): return - device = device_registry.deleted_devices[event.data["device_id"]] - - if entry.entry_id not in device.config_entries: - return - - macs = [c[1] for c in device.connections if c[0] == CONNECTION_NETWORK_MAC] + connections: set[tuple[str, str]] + if event.data["action"] == "update": + if "config_entries" not in event.data["changes"]: + return + + device = device_registry.async_get(device_id) + if not device: + # The device is already removed, do cleanup when we get "remove" event + return + if entry.entry_id in device.config_entries: + # Not removed from device + return + connections = device.connections + else: + deleted_device = device_registry.deleted_devices[event.data["device_id"]] + connections = deleted_device.connections + if entry.entry_id not in deleted_device.config_entries: + return + + macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] for mac in macs: await clear_discovery_topic( mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt ) hass.data[DATA_UNSUB].append( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) + hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_updated) ) async def start_platforms() -> None: @@ -135,7 +154,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(platform))() # deattach device triggers - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) devices = async_entries_for_config_entry(device_registry, entry.entry_id) for device in devices: await device_automation.async_remove_automations(hass, device.id) @@ -153,11 +172,13 @@ async def _remove_device( """Remove device from device registry.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) - if device is None: + if device is None or config_entry.entry_id not in device.config_entries: return - _LOGGER.debug("Removing tasmota device %s", mac) - device_registry.async_remove_device(device.id) + _LOGGER.debug("Removing tasmota from device %s", mac) + device_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) await clear_discovery_topic( mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt ) @@ -200,13 +221,13 @@ async def async_setup_device( @websocket_api.websocket_command( {vol.Required("type"): "tasmota/device/remove", vol.Required("device_id"): str} ) -@websocket_api.async_response -async def websocket_remove_device( +@callback +def websocket_remove_device( hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Delete device.""" device_id = msg["device_id"] - dev_registry = await hass.helpers.device_registry.async_get_registry() + dev_registry = dr.async_get(hass) if not (device := dev_registry.async_get(device_id)): connection.send_error( @@ -214,8 +235,9 @@ async def websocket_remove_device( ) return - for config_entry in device.config_entries: - config_entry = hass.config_entries.async_get_entry(config_entry) + for config_entry_id in device.config_entries: + config_entry = hass.config_entries.async_get_entry(config_entry_id) + assert config_entry # Only delete the device if it belongs to a Tasmota device entry if config_entry.domain == DOMAIN: dev_registry.async_remove_device(device_id) @@ -225,3 +247,11 @@ async def websocket_remove_device( connection.send_error( msg["id"], websocket_api.const.ERR_NOT_FOUND, "Non Tasmota device" ) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove Tasmota config entry from a device.""" + # Just return True, cleanup is done on when handling device registry events + return True diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 61efbb76e23bb..aca5a2848e321 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -20,7 +20,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType @@ -220,7 +220,7 @@ async def discovery_update(trigger_config: TasmotaTriggerConfig) -> None: hass, TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash), discovery_update ) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( set(), {(CONNECTION_NETWORK_MAC, tasmota_trigger.cfg.mac)}, diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 67aea199fe4ca..da9e809bd8b78 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -21,7 +21,7 @@ from homeassistant.components import sensor from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dev_reg +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_device @@ -61,7 +61,7 @@ async def async_start( ) -> None: """Start Tasmota device discovery.""" - async def _discover_entity( + def _discover_entity( tasmota_entity_config: TasmotaEntityConfig | None, discovery_hash: DiscoveryHashType, platform: str, @@ -69,7 +69,7 @@ async def _discover_entity( """Handle adding or updating a discovered entity.""" if not tasmota_entity_config: # Entity disabled, clean up entity registry - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) unique_id = unique_id_from_hash(discovery_hash) entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id) if entity_id: @@ -158,7 +158,7 @@ async def async_device_discovered(payload: dict, mac: str) -> None: for platform in PLATFORMS: tasmota_entities = tasmota_get_entities_for_platform(payload, platform) for (tasmota_entity_config, discovery_hash) in tasmota_entities: - await _discover_entity(tasmota_entity_config, discovery_hash, platform) + _discover_entity(tasmota_entity_config, discovery_hash, platform) async def async_sensors_discovered( sensors: list[tuple[TasmotaBaseSensorConfig, DiscoveryHashType]], mac: str @@ -166,10 +166,10 @@ async def async_sensors_discovered( """Handle discovery of (additional) sensors.""" platform = sensor.DOMAIN - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) device = device_registry.async_get_device( - set(), {(dev_reg.CONNECTION_NETWORK_MAC, mac)} + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) if device is None: @@ -186,7 +186,7 @@ async def async_sensors_discovered( for (tasmota_sensor_config, discovery_hash) in sensors: if tasmota_sensor_config: orphaned_entities.discard(tasmota_sensor_config.unique_id) - await _discover_entity(tasmota_sensor_config, discovery_hash, platform) + _discover_entity(tasmota_sensor_config, discovery_hash, platform) for unique_id in orphaned_entities: entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id) if entity_id: diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index bd30231396f99..a1f52517690fb 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["hatasmota"] } diff --git a/homeassistant/components/tasmota/translations/el.json b/homeassistant/components/tasmota/translations/el.json index f66d14e030a40..cb9bc10730c8f 100644 --- a/homeassistant/components/tasmota/translations/el.json +++ b/homeassistant/components/tasmota/translations/el.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "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": { "invalid_discovery_topic": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2." }, diff --git a/homeassistant/components/tasmota/translations/pt-BR.json b/homeassistant/components/tasmota/translations/pt-BR.json new file mode 100644 index 0000000000000..6fa1f064e8899 --- /dev/null +++ b/homeassistant/components/tasmota/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_discovery_topic": "Prefixo do t\u00f3pico de descoberta inv\u00e1lido." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Prefixo do t\u00f3pico de descoberta" + }, + "description": "Por favor, insira a configura\u00e7\u00e3o do Tasmota.", + "title": "Tasmota" + }, + "confirm": { + "description": "Deseja configurar o Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/uk.json b/homeassistant/components/tasmota/translations/uk.json index 6639a9c9626eb..5b57f950866b6 100644 --- a/homeassistant/components/tasmota/translations/uk.json +++ b/homeassistant/components/tasmota/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "invalid_discovery_topic": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f." diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 477eb0ffa9cde..3a11beed83949 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_discovery_topic": "\u63a2\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index 68edea9983851..06c2d4e0c6b81 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tautulli", "requirements": ["pytautulli==21.11.0"], "codeowners": ["@ludeeus"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pytautulli"] } diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index fd7e731194c99..b87ddc670c359 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -11,6 +11,11 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.telegram_bot import ( + ATTR_DISABLE_NOTIF, + ATTR_MESSAGE_TAG, + ATTR_PARSER, +) from homeassistant.const import ATTR_LOCATION from homeassistant.helpers.reload import setup_reload_service @@ -56,6 +61,21 @@ def send_message(self, message="", **kwargs): service_data.update({ATTR_MESSAGE: message}) data = kwargs.get(ATTR_DATA) + # Set message tag + if data is not None and ATTR_MESSAGE_TAG in data: + message_tag = data.get(ATTR_MESSAGE_TAG) + service_data.update({ATTR_MESSAGE_TAG: message_tag}) + + # Set disable_notification + if data is not None and ATTR_DISABLE_NOTIF in data: + disable_notification = data.get(ATTR_DISABLE_NOTIF) + service_data.update({ATTR_DISABLE_NOTIF: disable_notification}) + + # Set parse_mode + if data is not None and ATTR_PARSER in data: + parse_mode = data.get(ATTR_PARSER) + service_data.update({ATTR_PARSER: parse_mode}) + # Get keyboard info if data is not None and ATTR_KEYBOARD in data: keys = data.get(ATTR_KEYBOARD) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7c03ecab271b5..cebdd4f4573d6 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -63,6 +63,7 @@ ATTR_REPLY_TO_MSGID = "reply_to_message_id" ATTR_REPLYMARKUP = "reply_markup" ATTR_SHOW_ALERT = "show_alert" +ATTR_STICKER_ID = "sticker_id" ATTR_TARGET = "target" ATTR_TEXT = "text" ATTR_URL = "url" @@ -165,6 +166,10 @@ } ) +SERVICE_SCHEMA_SEND_STICKER = SERVICE_SCHEMA_SEND_FILE.extend( + {vol.Optional(ATTR_STICKER_ID): cv.string} +) + SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend( { vol.Required(ATTR_LONGITUDE): cv.template, @@ -228,7 +233,7 @@ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, - SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_STICKER, SERVICE_SEND_ANIMATION: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE, @@ -371,7 +376,6 @@ def _render_template_attr(data, attribute): ) elif msgtype in [ SERVICE_SEND_PHOTO, - SERVICE_SEND_STICKER, SERVICE_SEND_ANIMATION, SERVICE_SEND_VIDEO, SERVICE_SEND_VOICE, @@ -380,6 +384,10 @@ def _render_template_attr(data, attribute): await hass.async_add_executor_job( partial(notify_service.send_file, msgtype, **kwargs) ) + elif msgtype == SERVICE_SEND_STICKER: + await hass.async_add_executor_job( + partial(notify_service.send_sticker, **kwargs) + ) elif msgtype == SERVICE_SEND_LOCATION: await hass.async_add_executor_job( partial(notify_service.send_location, **kwargs) @@ -797,6 +805,25 @@ def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): else: _LOGGER.error("Can't send file with kwargs: %s", kwargs) + def send_sticker(self, target=None, **kwargs): + """Send a sticker from a telegram sticker pack.""" + params = self._get_msg_kwargs(kwargs) + stickerid = kwargs.get(ATTR_STICKER_ID) + if stickerid: + for chat_id in self._get_target_chat_ids(target): + self._send_msg( + self.bot.send_sticker, + "Error sending sticker", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + sticker=stickerid, + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + ) + else: + self.send_file(SERVICE_SEND_STICKER, target, **kwargs) + def send_location(self, latitude, longitude, target=None, **kwargs): """Send a location.""" latitude = float(latitude) diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index 048762903e1cb..d5eec8db55211 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -5,5 +5,6 @@ "requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"], "dependencies": ["http"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["telegram"] } diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index ea406cfdf9635..6afd42dffb81c 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -181,6 +181,12 @@ send_sticker: example: "/path/to/the/sticker.webp" selector: text: + sticker_id: + name: Sticker ID + description: ID of a sticker that exists on telegram servers + example: CAACAgIAAxkBAAEDDldhZD-hqWclr6krLq-FWSfCrGNmOQAC9gAD9HsZAAFeYY-ltPYnrCEE + selector: + text: username: name: Username description: Username for a URL which require HTTP authentication. diff --git a/homeassistant/components/tellduslive/translations/el.json b/homeassistant/components/tellduslive/translations/el.json new file mode 100644 index 0000000000000..c5948ef0f264a --- /dev/null +++ b/homeassistant/components/tellduslive/translations/el.json @@ -0,0 +1,26 @@ +{ + "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", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "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." + }, + "error": { + "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": { + "auth": { + "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 TelldusLive:\n 1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\n 2. \u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Telldus Live\n 3. \u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 **{\u03cc\u03bd\u03bf\u03bc\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2}** (\u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u039d\u03b1\u03b9**).\n 4. \u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u03a5\u03a0\u039f\u0392\u039f\u039b\u0397**.\n\n [\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd TelldusLive]({auth_url})", + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1\u03bd\u03c4\u03b9 \u03c4\u03bf\u03c5 TelldusLive" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u039a\u03b5\u03bd\u03cc", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/pt-BR.json b/homeassistant/components/tellduslive/translations/pt-BR.json index 036af4e1c45a7..1c94b12cc5979 100644 --- a/homeassistant/components/tellduslive/translations/pt-BR.json +++ b/homeassistant/components/tellduslive/translations/pt-BR.json @@ -1,8 +1,13 @@ { "config": { "abort": { - "authorize_url_timeout": "Tempo limite de gera\u00e7\u00e3o de url de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "unknown": "Erro inesperado", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "auth": { @@ -11,7 +16,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Vazio", "title": "Escolha o ponto final." diff --git a/homeassistant/components/tellduslive/translations/sk.json b/homeassistant/components/tellduslive/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index 5d8029ddcf529..bb0f9d32e3e96 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tellstick", "requirements": ["tellcore-net==0.4", "tellcore-py==1.1.2"], "codeowners": [], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["tellcore"] } diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index f59bfad92f406..9ad295d9ac505 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import telnetlib +from typing import Any import voluptuous as vol @@ -26,6 +27,7 @@ from homeassistant.core import HomeAssistant 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 _LOGGER = logging.getLogger(__name__) @@ -60,27 +62,26 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Find and return switches controlled by telnet commands.""" - devices = config.get(CONF_SWITCHES, {}) + devices: dict[str, Any] = config[CONF_SWITCHES] switches = [] for object_id, device_config in devices.items(): - value_template = device_config.get(CONF_VALUE_TEMPLATE) + value_template: Template | None = device_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass switches.append( TelnetSwitch( - hass, object_id, - device_config.get(CONF_RESOURCE), - device_config.get(CONF_PORT), + device_config[CONF_RESOURCE], + device_config[CONF_PORT], device_config.get(CONF_NAME, object_id), - device_config.get(CONF_COMMAND_ON), - device_config.get(CONF_COMMAND_OFF), + device_config[CONF_COMMAND_ON], + device_config[CONF_COMMAND_OFF], device_config.get(CONF_COMMAND_STATE), value_template, - device_config.get(CONF_TIMEOUT), + device_config[CONF_TIMEOUT], ) ) @@ -96,82 +97,65 @@ class TelnetSwitch(SwitchEntity): def __init__( self, - hass, - object_id, - resource, - port, - friendly_name, - command_on, - command_off, - command_state, - value_template, - timeout, - ): + object_id: str, + resource: str, + port: int, + friendly_name: str, + command_on: str, + command_off: str, + command_state: str | None, + value_template: Template | None, + timeout: float, + ) -> None: """Initialize the switch.""" - self._hass = hass self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._resource = resource self._port = port - self._name = friendly_name - self._state = False + self._attr_name = friendly_name + self._attr_is_on = False self._command_on = command_on self._command_off = command_off self._command_state = command_state self._value_template = value_template self._timeout = timeout + self._attr_should_poll = bool(command_state) + self._attr_assumed_state = bool(command_state is None) - def _telnet_command(self, command): + def _telnet_command(self, command: str) -> str | None: try: telnet = telnetlib.Telnet(self._resource, self._port) telnet.write(command.encode("ASCII") + b"\r") response = telnet.read_until(b"\r", timeout=self._timeout) - _LOGGER.debug("telnet response: %s", response.decode("ASCII").strip()) - return response.decode("ASCII").strip() except OSError as error: _LOGGER.error( 'Command "%s" failed with exception: %s', command, repr(error) ) return None + _LOGGER.debug("telnet response: %s", response.decode("ASCII").strip()) + return response.decode("ASCII").strip() - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Only poll if we have state command.""" - return self._command_state is not None - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): - """Return true if no state command is defined, false otherwise.""" - return self._command_state is None - - def update(self): + def update(self) -> None: """Update device state.""" + if not self._command_state: + return response = self._telnet_command(self._command_state) - if response: + if response and self._value_template: rendered = self._value_template.render_with_possible_json_value(response) - self._state = rendered == "True" else: _LOGGER.warning("Empty response for command: %s", self._command_state) + return None + self._attr_is_on = rendered == "True" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs) -> None: """Turn the device on.""" self._telnet_command(self._command_on) if self.assumed_state: - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs) -> None: """Turn the device off.""" self._telnet_command(self._command_off) if self.assumed_state: - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index 0443987a87b55..b71bbe915632b 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": ["temperusb==1.5.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyusb", "temperusb"] } diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 318b457c24ba2..a1231708b9100 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -61,7 +61,7 @@ async def _reload_config(call: Event | ServiceCall) -> None: return True -async def _process_config(hass, hass_config): +async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: """Process config.""" coordinators: list[TriggerUpdateCoordinator] | None = hass.data.pop(DOMAIN, None) @@ -126,7 +126,7 @@ def async_remove(self): if self._unsub_trigger: self._unsub_trigger() - async def async_setup(self: HomeAssistant, hass_config: ConfigType) -> bool: + async def async_setup(self, hass_config: ConfigType) -> None: """Set up the trigger and create entities.""" if self.hass.state == CoreState.running: await self._attach_triggers() diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index d25784688860d..4c080c736d04f 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -31,7 +31,7 @@ CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -300,12 +300,12 @@ def __init__( self._to_render_simple.append(key) self._parse_result.add(key) - self._delay_cancel = None + self._delay_cancel: CALLBACK_TYPE | None = None self._auto_off_cancel = None - self._state = None + self._state: bool | None = None @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return state of the sensor.""" return self._state diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index 86f481d7032c8..d6f41649734ba 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -63,7 +63,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the template button.""" - if "coordinator" in discovery_info: + if not discovery_info or "coordinator" in discovery_info: raise PlatformNotReady( "The template button platform doesn't support trigger entities" ) @@ -86,6 +86,7 @@ def __init__( ) -> None: """Initialize the button.""" super().__init__(hass, config=config, unique_id=unique_id) + assert self._attr_name is not None self._command_press = Script(hass, config[CONF_PRESS], self._attr_name, DOMAIN) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_state = None diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 9d0a32f3eddd3..1ddd37ba7bc08 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -63,7 +64,6 @@ CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" _VALID_STATES = [STATE_ON, STATE_OFF] -_VALID_OSC = [True, False] _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.All( @@ -273,8 +273,7 @@ async def async_turn_on( elif speed is not None: await self.async_set_speed(speed) - # pylint: disable=arguments-differ - async def async_turn_off(self) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" await self._off_script.async_run(context=self._context) self._state = STATE_OFF @@ -316,17 +315,10 @@ async def async_oscillate(self, oscillating: bool) -> None: if self._set_oscillating_script is None: return - if oscillating in _VALID_OSC: - self._oscillating = oscillating - await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating}, context=self._context - ) - else: - _LOGGER.error( - "Received invalid oscillating value: %s. Expected: %s", - oscillating, - ", ".join(_VALID_OSC), - ) + self._oscillating = oscillating + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating}, context=self._context + ) async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 036485cd363c8..89adb45a6d0a0 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -108,6 +108,7 @@ def __init__( ) -> None: """Initialize the number.""" super().__init__(hass, config=config, unique_id=unique_id) + assert self._attr_name is not None self._value_template = config[CONF_STATE] self._command_set_value = Script( hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 2829583531826..fa2668c1e8151 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -102,13 +102,14 @@ def __init__( ) -> None: """Initialize the select.""" super().__init__(hass, config=config, unique_id=unique_id) + assert self._attr_name is not None self._value_template = config[CONF_STATE] self._command_select_option = Script( hass, config[CONF_SELECT_OPTION], self._attr_name, DOMAIN ) self._options_template = config[ATTR_OPTIONS] self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] - self._attr_options = None + self._attr_options = [] self._attr_current_option = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index ee4b62447573f..5e592e5d71730 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -16,13 +16,13 @@ CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, + EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback +from homeassistant.core import CoreState, Event, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - Event, TrackTemplate, TrackTemplateResult, async_track_template_result, @@ -94,7 +94,7 @@ def rewrite_common_legacy_to_modern_conf( entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] = None -) -> list[dict]: +) -> dict[str, Any]: """Rewrite legacy config.""" entity_cfg = {**entity_cfg} if extra_legacy_fields is None: @@ -176,10 +176,12 @@ def handle_result( if self.none_on_template_error: self._default_update(result) else: + assert self.on_update self.on_update(result) return if not self.validator: + assert self.on_update self.on_update(result) return @@ -197,9 +199,11 @@ def handle_result( self._entity.entity_id, ex.msg, ) + assert self.on_update self.on_update(None) return + assert self.on_update self.on_update(validated) return @@ -321,11 +325,11 @@ def add_template_attribute( """ assert self.hass is not None, "hass cannot be None" template.hass = self.hass - attribute = _TemplateAttribute( + template_attribute = _TemplateAttribute( self, attribute, template, validator, on_update, none_on_template_error ) self._template_attrs.setdefault(template, []) - self._template_attrs[template].append(attribute) + self._template_attrs[template].append(template_attribute) @callback def _handle_results( @@ -362,7 +366,7 @@ def _handle_results( self.async_write_ha_state() async def _async_template_startup(self, *_) -> None: - template_var_tups = [] + template_var_tups: list[TrackTemplate] = [] has_availability_template = False for template, attributes in self._template_attrs.items(): template_var_tup = TrackTemplate(template, None) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index d4bc96e43bf4c..2768609e65da5 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -12,6 +12,7 @@ CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import TemplateError from homeassistant.helpers import template, update_coordinator from . import TriggerUpdateCoordinator @@ -36,6 +37,7 @@ def __init__( entity_unique_id = config.get(CONF_UNIQUE_ID) + self._unique_id: str | None if entity_unique_id and coordinator.unique_id: self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}" else: @@ -45,7 +47,7 @@ def __init__( self._static_rendered = {} self._to_render_simple = [] - self._to_render_complex = [] + self._to_render_complex: list[str] = [] for itm in ( CONF_NAME, @@ -148,7 +150,7 @@ def _process_data(self) -> None: ) self._rendered = rendered - except template.TemplateError as err: + except TemplateError as err: logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( "Error rendering %s template for %s: %s", key, self.entity_id, err ) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index b9b0ff6b5c5b4..6d8f50c81bfcb 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,5 +10,6 @@ "pillow==9.0.1" ], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tensorflow"] } diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 742b0a34b177d..c4c7c551375cb 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -107,7 +107,7 @@ def get_poll_interval(entry: ConfigEntry) -> timedelta: ) -async def update_listener(hass, entry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" wall_connector_data: WallConnectorData = hass.data[DOMAIN][entry.entry_id] wall_connector_data.update_coordinator.update_interval = get_poll_interval(entry) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index 8e86fa3d2f889..28c2d222f3a21 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -21,5 +21,6 @@ "codeowners": [ "@einarhauks" ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tesla_wall_connector"] } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/cs.json b/homeassistant/components/tesla_wall_connector/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/el.json b/homeassistant/components/tesla_wall_connector/translations/el.json index 286fbaee105e9..3a835ab5d5737 100644 --- a/homeassistant/components/tesla_wall_connector/translations/el.json +++ b/homeassistant/components/tesla_wall_connector/translations/el.json @@ -1,8 +1,18 @@ { "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" + }, "flow_title": "{serial_number} ({host})", "step": { "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Tesla Wall Connector" } } diff --git a/homeassistant/components/tesla_wall_connector/translations/pt-BR.json b/homeassistant/components/tesla_wall_connector/translations/pt-BR.json new file mode 100644 index 0000000000000..da2c9d50f34f8 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{serial_number} ( {host} )", + "step": { + "user": { + "data": { + "host": "Nome do host" + }, + "title": "Configurar o conector de parede Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "title": "Configurar op\u00e7\u00f5es para o Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index aa9a87413907b..d9f2052bd5300 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", "requirements": ["stringcase==1.2.0", "thermoworks_smoke==0.1.8"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["thermoworks_smoke"] } diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 3ac2e7e4b2523..f14ea25768be4 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": ["thingspeak==1.0.0"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["thingspeak"] } diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index cb87c1ea8a375..33081cb967d94 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", "requirements": ["pythinkingcleaner==0.0.3"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pythinkingcleaner"] } diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 2f5927442a2d4..9b84b9a54c286 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,9 +3,10 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.21.7"], + "requirements": ["pyTibber==0.22.1"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["tibber"] } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index eeb072e3a625d..12bcec295d06e 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -210,7 +210,7 @@ ), SensorEntityDescription( key="peak_hour", - name="Month peak hour consumption", + name="Monthly peak hour consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), @@ -253,17 +253,17 @@ async def async_setup_entry( if home.has_active_subscription: entities.append(TibberSensorElPrice(home)) + if coordinator is None: + coordinator = TibberDataCoordinator(hass, tibber_connection) + for entity_description in SENSORS: + entities.append(TibberDataSensor(home, coordinator, entity_description)) + if home.has_real_time_consumption: await home.rt_subscribe( TibberRtDataCoordinator( async_add_entities, home, hass ).async_set_updated_data ) - if home.has_active_subscription and not home.has_real_time_consumption: - if coordinator is None: - coordinator = TibberDataCoordinator(hass, tibber_connection) - for entity_description in SENSORS: - entities.append(TibberDataSensor(home, coordinator, entity_description)) # migrate old_id = home.info["viewer"]["home"]["meteringPointData"]["consumptionEan"] @@ -464,7 +464,7 @@ def _handle_coordinator_update(self) -> None: ts_local = dt_util.parse_datetime(live_measurement["timestamp"]) if ts_local is not None: if self.last_reset is None or ( - state < 0.5 * self.native_value # type: ignore # native_value is float + state < 0.5 * self.native_value # type: ignore[operator] # native_value is float and ( ts_local.hour == 0 or (ts_local - self.last_reset) > timedelta(hours=24) @@ -547,7 +547,7 @@ def __init__(self, hass, tibber_connection): hass, _LOGGER, name=f"Tibber {tibber_connection.name}", - update_interval=timedelta(hours=1), + update_interval=timedelta(minutes=20), ) self._tibber_connection = tibber_connection diff --git a/homeassistant/components/tibber/translations/el.json b/homeassistant/components/tibber/translations/el.json new file mode 100644 index 0000000000000..0ad70ba6ac762 --- /dev/null +++ b/homeassistant/components/tibber/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\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", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/pt-BR.json b/homeassistant/components/tibber/translations/pt-BR.json new file mode 100644 index 0000000000000..7ff663470094c --- /dev/null +++ b/homeassistant/components/tibber/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_access_token": "Token de acesso inv\u00e1lido", + "timeout": "Tempo limite de conex\u00e3o com o Tibber" + }, + "step": { + "user": { + "data": { + "access_token": "Token de acesso" + }, + "description": "Insira seu token de acesso em https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/sk.json b/homeassistant/components/tibber/translations/sk.json new file mode 100644 index 0000000000000..13ca7333f5c2c --- /dev/null +++ b/homeassistant/components/tibber/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index 8e332df8f625f..39d4d808a1565 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tikteck", "requirements": ["tikteck==0.4"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tikteck"] } diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 58bb929e44689..e14244530757b 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, LOGGER @@ -75,7 +74,7 @@ async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_reauth(self, config: ConfigType) -> FlowResult: + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index b8e1e14ac8bfc..dd0e78007f634 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": ["pytile==2022.02.0"], "codeowners": ["@bachya"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pytile"] } diff --git a/homeassistant/components/tile/translations/el.json b/homeassistant/components/tile/translations/el.json new file mode 100644 index 0000000000000..57bc15272c9f8 --- /dev/null +++ b/homeassistant/components/tile/translations/el.json @@ -0,0 +1,36 @@ +{ + "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", + "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_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_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Tile" + }, + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b1\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03ce\u03bd Tiles" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Tile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/pt-BR.json b/homeassistant/components/tile/translations/pt-BR.json new file mode 100644 index 0000000000000..e3a4aa67c026f --- /dev/null +++ b/homeassistant/components/tile/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "title": "Re-autenticar o bloco" + }, + "user": { + "data": { + "password": "Senha", + "username": "Email" + }, + "title": "Configurar bloco" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Mostrar blocos inativos" + }, + "title": "Configurar bloco" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/sk.json b/homeassistant/components/tile/translations/sk.json new file mode 100644 index 0000000000000..d30ed436a4fd2 --- /dev/null +++ b/homeassistant/components/tile/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tmb/manifest.json b/homeassistant/components/tmb/manifest.json index 4032b7e27d6c9..a9b4da9b2fddb 100644 --- a/homeassistant/components/tmb/manifest.json +++ b/homeassistant/components/tmb/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tmb", "requirements": ["tmb==0.0.4"], "codeowners": ["@alemuro"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tmb"] } diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 09cd080b4d770..a00819638f3ce 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": ["todoist-python==8.0.0"], "codeowners": ["@boralyl"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["todoist"] } diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index 83a0ba6fbe342..e530c67b93002 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -5,5 +5,6 @@ "requirements": ["VL53L1X2==0.1.5"], "dependencies": ["rpi_gpio"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["VL53L1X2"] } diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 6e95089ccbea2..6f17379dfbf94 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -28,6 +28,7 @@ Platform.CLIMATE, Platform.FAN, Platform.LIGHT, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, ] diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 659dfcbda1681..4c02cdb74dce0 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -144,8 +144,7 @@ def set_humidity(self, humidity: float) -> None: def set_temperature(self, **kwargs: Any) -> None: """Set desired target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self.coordinator.client.set_target_temperature(round(temperature)) diff --git a/homeassistant/components/tolo/const.py b/homeassistant/components/tolo/const.py index bfd700bb95529..77bee92d521ac 100644 --- a/homeassistant/components/tolo/const.py +++ b/homeassistant/components/tolo/const.py @@ -11,3 +11,7 @@ DEFAULT_MAX_HUMIDITY = 99 DEFAULT_MIN_HUMIDITY = 60 + +POWER_TIMER_MAX = 60 +SALT_BATH_TIMER_MAX = 60 +FAN_TIMER_MAX = 60 diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index 63e87ebf876fe..aa60958591c89 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -10,5 +10,6 @@ "@MatthiasLohr" ], "iot_class": "local_polling", - "dhcp": [{"hostname": "usr-tcp232-ed2"}] + "dhcp": [{"hostname": "usr-tcp232-ed2"}], + "loggers": ["tololib"] } \ No newline at end of file diff --git a/homeassistant/components/tolo/number.py b/homeassistant/components/tolo/number.py new file mode 100644 index 0000000000000..85d80756020d8 --- /dev/null +++ b/homeassistant/components/tolo/number.py @@ -0,0 +1,111 @@ +"""TOLO Sauna number controls.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from tololib import ToloClient +from tololib.message_info import SettingsInfo + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_MINUTES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN, FAN_TIMER_MAX, POWER_TIMER_MAX, SALT_BATH_TIMER_MAX + + +@dataclass +class ToloNumberEntityDescriptionBase: + """Required values when describing TOLO Number entities.""" + + getter: Callable[[SettingsInfo], int | None] + setter: Callable[[ToloClient, int | None], Any] + + +@dataclass +class ToloNumberEntityDescription( + NumberEntityDescription, ToloNumberEntityDescriptionBase +): + """Class describing TOLO Number entities.""" + + entity_category = EntityCategory.CONFIG + min_value = 0 + step = 1 + + +NUMBERS = ( + ToloNumberEntityDescription( + key="power_timer", + icon="mdi:power-settings", + name="Power Timer", + unit_of_measurement=TIME_MINUTES, + max_value=POWER_TIMER_MAX, + getter=lambda settings: settings.power_timer, + setter=lambda client, value: client.set_power_timer(value), + ), + ToloNumberEntityDescription( + key="salt_bath_timer", + icon="mdi:shaker-outline", + name="Salt Bath Timer", + unit_of_measurement=TIME_MINUTES, + max_value=SALT_BATH_TIMER_MAX, + getter=lambda settings: settings.salt_bath_timer, + setter=lambda client, value: client.set_salt_bath_timer(value), + ), + ToloNumberEntityDescription( + key="fan_timer", + icon="mdi:fan-auto", + name="Fan Timer", + unit_of_measurement=TIME_MINUTES, + max_value=FAN_TIMER_MAX, + getter=lambda settings: settings.fan_timer, + setter=lambda client, value: client.set_fan_timer(value), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up number controls for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + ToloNumberEntity(coordinator, entry, description) for description in NUMBERS + ) + + +class ToloNumberEntity(ToloSaunaCoordinatorEntity, NumberEntity): + """TOLO Number entity.""" + + entity_description: ToloNumberEntityDescription + + def __init__( + self, + coordinator: ToloSaunaUpdateCoordinator, + entry: ConfigEntry, + entity_description: ToloNumberEntityDescription, + ) -> None: + """Initialize TOLO Number entity.""" + super().__init__(coordinator, entry) + self.entity_description = entity_description + self._attr_unique_id = f"{entry.entry_id}_{entity_description.key}" + + @property + def value(self) -> float: + """Return the value of this TOLO Number entity.""" + return self.entity_description.getter(self.coordinator.data.settings) or 0 + + def set_value(self, value: float) -> None: + """Set the value of this TOLO Number entity.""" + int_value = int(value) + if int_value == 0: + self.entity_description.setter(self.coordinator.client, None) + return + self.entity_description.setter(self.coordinator.client, int_value) diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py index bcdb7db016535..714667cc1f83b 100644 --- a/homeassistant/components/tolo/sensor.py +++ b/homeassistant/components/tolo/sensor.py @@ -1,12 +1,19 @@ """TOLO Sauna (non-binary, general) sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from tololib.message_info import SettingsInfo, StatusInfo from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -15,6 +22,75 @@ from .const import DOMAIN +@dataclass +class ToloSensorEntityDescriptionBase: + """Required values when describing TOLO Sensor entities.""" + + getter: Callable[[StatusInfo], int | None] + availability_checker: Callable[[SettingsInfo, StatusInfo], bool] | None + + +@dataclass +class ToloSensorEntityDescription( + SensorEntityDescription, ToloSensorEntityDescriptionBase +): + """Class describing TOLO Sensor entities.""" + + state_class = SensorStateClass.MEASUREMENT + + +SENSORS = ( + ToloSensorEntityDescription( + key="water_level", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:waves-arrow-up", + name="Water Level", + native_unit_of_measurement=PERCENTAGE, + getter=lambda status: status.water_level_percent, + availability_checker=None, + ), + ToloSensorEntityDescription( + key="tank_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + name="Tank Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + getter=lambda status: status.tank_temperature, + availability_checker=None, + ), + ToloSensorEntityDescription( + key="power_timer_remaining", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:power-settings", + name="Power Timer", + native_unit_of_measurement=TIME_MINUTES, + getter=lambda status: status.power_timer, + availability_checker=lambda settings, status: status.power_on + and settings.power_timer is not None, + ), + ToloSensorEntityDescription( + key="salt_bath_timer_remaining", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:shaker-outline", + name="Salt Bath Timer", + native_unit_of_measurement=TIME_MINUTES, + getter=lambda status: status.salt_bath_timer, + availability_checker=lambda settings, status: status.salt_bath_on + and settings.salt_bath_timer is not None, + ), + ToloSensorEntityDescription( + key="fan_timer_remaining", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:fan-auto", + name="Fan Timer", + native_unit_of_measurement=TIME_MINUTES, + getter=lambda status: status.fan_timer, + availability_checker=lambda settings, status: status.fan_on + and settings.fan_timer is not None, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -23,54 +99,36 @@ async def async_setup_entry( """Set up (non-binary, general) sensors for TOLO Sauna.""" coordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - ToloWaterLevelSensor(coordinator, entry), - ToloTankTemperatureSensor(coordinator, entry), - ] + ToloSensorEntity(coordinator, entry, description) for description in SENSORS ) -class ToloWaterLevelSensor(ToloSaunaCoordinatorEntity, SensorEntity): - """Sensor for tank water level.""" +class ToloSensorEntity(ToloSaunaCoordinatorEntity, SensorEntity): + """TOLO Number entity.""" - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_name = "Water Level" - _attr_icon = "mdi:waves-arrow-up" - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = PERCENTAGE + entity_description: ToloSensorEntityDescription def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, + coordinator: ToloSaunaUpdateCoordinator, + entry: ConfigEntry, + entity_description: ToloSensorEntityDescription, ) -> None: - """Initialize TOLO Sauna tank water level sensor entity.""" + """Initialize TOLO Number entity.""" super().__init__(coordinator, entry) - - self._attr_unique_id = f"{entry.entry_id}_water_level" + self.entity_description = entity_description + self._attr_unique_id = f"{entry.entry_id}_{entity_description.key}" @property - def native_value(self) -> int: - """Return current tank water level.""" - return self.coordinator.data.status.water_level_percent - - -class ToloTankTemperatureSensor(ToloSaunaCoordinatorEntity, SensorEntity): - """Sensor for tank temperature.""" - - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_name = "Tank Temperature" - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = TEMP_CELSIUS - - def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry - ) -> None: - """Initialize TOLO Sauna tank temperature sensor entity.""" - super().__init__(coordinator, entry) - - self._attr_unique_id = f"{entry.entry_id}_tank_temperature" + def available(self) -> bool: + """Return availability of the TOLO sensor.""" + if self.entity_description.availability_checker is None: + return super().available + return self.entity_description.availability_checker( + self.coordinator.data.settings, self.coordinator.data.status + ) @property - def native_value(self) -> int: - """Return current tank temperature.""" - return self.coordinator.data.status.tank_temperature + def native_value(self) -> int | None: + """Return native value of the TOLO sensor.""" + return self.entity_description.getter(self.coordinator.data.status) diff --git a/homeassistant/components/tolo/translations/el.json b/homeassistant/components/tolo/translations/el.json index f214d3d7cfe9c..6b745e37a8ea7 100644 --- a/homeassistant/components/tolo/translations/el.json +++ b/homeassistant/components/tolo/translations/el.json @@ -1,7 +1,21 @@ { "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", + "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": "{name}", "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;" + }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \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 \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 TOLO Sauna." } } diff --git a/homeassistant/components/tolo/translations/pt-BR.json b/homeassistant/components/tolo/translations/pt-BR.json new file mode 100644 index 0000000000000..c86e10b4e8e94 --- /dev/null +++ b/homeassistant/components/tolo/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Digite o nome do host ou o endere\u00e7o IP do seu dispositivo TOLO Sauna." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.pt-BR.json b/homeassistant/components/tolo/translations/select.pt-BR.json new file mode 100644 index 0000000000000..61ee349aa2fad --- /dev/null +++ b/homeassistant/components/tolo/translations/select.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "autom\u00e1tico", + "manual": "manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index dc32b6bfac599..f6dc4ae284329 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -13,5 +13,6 @@ "macaddress": "74C63B*" } ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["toonapi"] } diff --git a/homeassistant/components/toon/translations/el.json b/homeassistant/components/toon/translations/el.json new file mode 100644 index 0000000000000..9ef53ffc9b728 --- /dev/null +++ b/homeassistant/components/toon/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af.", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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_agreements": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03b8\u03cc\u03bd\u03b5\u03c2 Toon.", + "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} )", + "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." + }, + "step": { + "agreement": { + "data": { + "agreement": "\u03a3\u03c5\u03bc\u03c6\u03c9\u03bd\u03af\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5.", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03af\u03b1 \u03c3\u03b1\u03c2" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03bd\u03bf\u03b9\u03ba\u03b9\u03b1\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/pt-BR.json b/homeassistant/components/toon/translations/pt-BR.json index 2e12ac49a8aae..dc450b8f5ae6d 100644 --- a/homeassistant/components/toon/translations/pt-BR.json +++ b/homeassistant/components/toon/translations/pt-BR.json @@ -1,7 +1,24 @@ { "config": { "abort": { - "no_agreements": "Esta conta n\u00e3o possui exibi\u00e7\u00f5es Toon." + "already_configured": "A conta j\u00e1 foi configurada", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_agreements": "Esta conta n\u00e3o possui exibi\u00e7\u00f5es Toon.", + "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})", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "step": { + "agreement": { + "data": { + "agreement": "Acordo" + }, + "description": "Selecione o endere\u00e7o do contrato que voc\u00ea deseja adicionar.", + "title": "Selecione seu contrato" + }, + "pick_implementation": { + "title": "Escolha seu locat\u00e1rio para autenticar" + } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index d3be51f91d264..d2a7708067254 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,9 +2,10 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.1"], + "requirements": ["total_connect_client==2022.2.1"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["total_connect_client"] } diff --git a/homeassistant/components/totalconnect/translations/el.json b/homeassistant/components/totalconnect/translations/el.json index 49156d1cbc20e..e2e19a56b0a99 100644 --- a/homeassistant/components/totalconnect/translations/el.json +++ b/homeassistant/components/totalconnect/translations/el.json @@ -1,13 +1,32 @@ { "config": { "abort": { - "no_locations": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 TotalConnect" + "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", + "no_locations": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 TotalConnect", + "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_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "usercode": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b4\u03b5\u03bd \u03b9\u03c3\u03c7\u03cd\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" }, "step": { + "locations": { + "data": { + "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "usercode": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 {location_id}", + "title": "\u039a\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u03a4\u03bf Total Connect \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \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" + }, "user": { + "data": { + "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" + }, "title": "Total Connect" } } diff --git a/homeassistant/components/totalconnect/translations/pt-BR.json b/homeassistant/components/totalconnect/translations/pt-BR.json index 432a49cacf675..47dfe5437e0d8 100644 --- a/homeassistant/components/totalconnect/translations/pt-BR.json +++ b/homeassistant/components/totalconnect/translations/pt-BR.json @@ -1,11 +1,30 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "no_locations": "Nenhum local est\u00e1 dispon\u00edvel para este usu\u00e1rio, verifique as configura\u00e7\u00f5es do TotalConnect", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "usercode": "C\u00f3digo de usu\u00e1rio n\u00e3o \u00e9 v\u00e1lido para este usu\u00e1rio neste local" }, "step": { + "locations": { + "data": { + "location": "Localiza\u00e7\u00e3o", + "usercode": "C\u00f3digo de usu\u00e1rio" + }, + "description": "Insira o c\u00f3digo de usu\u00e1rio para este usu\u00e1rio no local {location_id}", + "title": "C\u00f3digos de usu\u00e1rio de localiza\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "Total Connect precisa re-autenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { + "password": "Senha", "username": "Usu\u00e1rio" }, "title": "Total Connect" diff --git a/homeassistant/components/totalconnect/translations/sk.json b/homeassistant/components/totalconnect/translations/sk.json new file mode 100644 index 0000000000000..59d045e76037b --- /dev/null +++ b/homeassistant/components/totalconnect/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "locations": { + "data": { + "location": "Umiestnenie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index 1ea02f29ae284..5d1ef4cc0dcb8 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/touchline", "requirements": ["pytouchline==0.7"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pytouchline"] } diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index e6b4c4aceab77..33b03109cd8ae 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -39,7 +39,7 @@ def async_trigger_discovery( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ CONF_NAME: device.alias, CONF_HOST: device.host, @@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: SmartDevice = hass_data[entry.entry_id].device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) - await device.protocol.close() # type: ignore + await device.protocol.close() # type: ignore[no-untyped-call] return unload_ok diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 5cb351989bea0..8b05f90041ab2 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -35,10 +35,10 @@ async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowRes discovery_info.ip, discovery_info.macaddress ) - async def async_step_discovery( + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" + """Handle integration discovery.""" return await self._async_handle_discovery( discovery_info[CONF_HOST], discovery_info[CONF_MAC] ) diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py new file mode 100644 index 0000000000000..5771bee5bd3fc --- /dev/null +++ b/homeassistant/components/tplink/diagnostics.py @@ -0,0 +1,46 @@ +"""Diagnostics support for TPLink.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import TPLinkDataUpdateCoordinator + +TO_REDACT = { + # Entry fields + "unique_id", # based on mac address + # Device identifiers + "alias", + "mac", + "mic_mac", + "host", + "hwId", + "oemId", + "deviceId", + # Device location + "latitude", + "latitude_i", + "longitude", + "longitude_i", + # Cloud connectivity info + "username", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + device = coordinator.device + + data = {} + data[ + "device_last_response" + ] = device._last_update # pylint: disable=protected-access + + return async_redact_data(data, TO_REDACT) diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index f04a7459543e9..4380b1397b6a4 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -54,6 +54,7 @@ def device_info(self) -> DeviceInfo: model=self.device.model, name=self.device.alias, sw_version=self.device.hw_info["sw_ver"], + hw_version=self.device.hw_info["hw_ver"], ) @property diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 5e0b98c4ddb81..20f2e9dc171ef 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,7 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + {"registered_devices": true}, { "hostname": "k[lp]*", "macaddress": "60A4B7*" @@ -117,5 +118,6 @@ "hostname": "lb*", "macaddress": "B09575*" } - ] + ], + "loggers": ["kasa"] } diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 823d37267d632..451ec6d5f8b05 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -28,7 +28,7 @@ async def async_setup_entry( """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] device = cast(SmartPlug, coordinator.device) - if not device.is_plug and not device.is_strip: + if not device.is_plug and not device.is_strip and not device.is_dimmer: return entities: list = [] if device.is_strip: @@ -36,7 +36,7 @@ async def async_setup_entry( _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) for child in device.children: entities.append(SmartPlugSwitch(child, coordinator)) - else: + elif device.is_plug: entities.append(SmartPlugSwitch(device, coordinator)) entities.append(SmartPlugLedSwitch(device, coordinator)) diff --git a/homeassistant/components/tplink/translations/el.json b/homeassistant/components/tplink/translations/el.json index 78f4ac3095d44..bea659e039a77 100644 --- a/homeassistant/components/tplink/translations/el.json +++ b/homeassistant/components/tplink/translations/el.json @@ -1,8 +1,31 @@ { "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", + "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": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 TP-Link;" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} {model} ({host});" + }, + "pick_device": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0395\u03ac\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/tplink/translations/pt-BR.json b/homeassistant/components/tplink/translations/pt-BR.json index f4852405726a1..a034b44b761d6 100644 --- a/homeassistant/components/tplink/translations/pt-BR.json +++ b/homeassistant/components/tplink/translations/pt-BR.json @@ -1,12 +1,31 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede.", - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 necess\u00e1ria." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Deseja configurar dispositivos inteligentes TP-Link?" + }, + "discovery_confirm": { + "description": "Deseja configurar {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para encontrar dispositivos." } } } diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json index cfeaf049675f2..abbfc076f7e1c 100644 --- a/homeassistant/components/tplink/translations/uk.json +++ b/homeassistant/components/tplink/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index 153783b1b9094..bfca7643b329c 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index c18ccbb61067a..63e20212005da 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "requirements": ["tp-connected==0.0.4"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["tp_connected"] } diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 77a8511a671ad..7f0df1b1f3fbf 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pytraccar==0.10.0", "stringcase==1.2.0"], "dependencies": ["webhook"], "codeowners": ["@ludeeus"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pytraccar"] } diff --git a/homeassistant/components/traccar/translations/bg.json b/homeassistant/components/traccar/translations/bg.json index 3859cbda43080..3444f6c0fdd14 100644 --- a/homeassistant/components/traccar/translations/bg.json +++ b/homeassistant/components/traccar/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/traccar/translations/ca.json b/homeassistant/components/traccar/translations/ca.json index 62c15e0ca2009..3f8a1b423b3c1 100644 --- a/homeassistant/components/traccar/translations/ca.json +++ b/homeassistant/components/traccar/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json index 3e94aaeb4c5ae..0d372da549f6b 100644 --- a/homeassistant/components/traccar/translations/de.json +++ b/homeassistant/components/traccar/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/traccar/translations/el.json b/homeassistant/components/traccar/translations/el.json index d82d7a5500cfa..f4760538f239d 100644 --- a/homeassistant/components/traccar/translations/el.json +++ b/homeassistant/components/traccar/translations/el.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." }, "create_entry": { "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 webhook \u03c3\u03c4\u03bf Traccar.\n\n\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL: `{webhook_url}`\n\n\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \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." diff --git a/homeassistant/components/traccar/translations/en.json b/homeassistant/components/traccar/translations/en.json index c6d7f0f189245..5a6d8eb2ccade 100644 --- a/homeassistant/components/traccar/translations/en.json +++ b/homeassistant/components/traccar/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/traccar/translations/et.json b/homeassistant/components/traccar/translations/et.json index e2c4ad68a8d85..beace8108c73a 100644 --- a/homeassistant/components/traccar/translations/et.json +++ b/homeassistant/components/traccar/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/traccar/translations/fr.json b/homeassistant/components/traccar/translations/fr.json index 3c32100078dc4..b3e9684424bb8 100644 --- a/homeassistant/components/traccar/translations/fr.json +++ b/homeassistant/components/traccar/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/traccar/translations/he.json b/homeassistant/components/traccar/translations/he.json index ebee9aee97649..55d9377f8d229 100644 --- a/homeassistant/components/traccar/translations/he.json +++ b/homeassistant/components/traccar/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." } diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index 902b4ea523191..6b80f58bc977d 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/traccar/translations/id.json b/homeassistant/components/traccar/translations/id.json index 573b73570c231..17912370a99c6 100644 --- a/homeassistant/components/traccar/translations/id.json +++ b/homeassistant/components/traccar/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/traccar/translations/it.json b/homeassistant/components/traccar/translations/it.json index cee10e5f9fe46..c70dff73bea94 100644 --- a/homeassistant/components/traccar/translations/it.json +++ b/homeassistant/components/traccar/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index 4338175fda65c..c635e23fbe0de 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/traccar/translations/nb.json b/homeassistant/components/traccar/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/traccar/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/nl.json b/homeassistant/components/traccar/translations/nl.json index 0b4563d69fcfa..45a70a42b435e 100644 --- a/homeassistant/components/traccar/translations/nl.json +++ b/homeassistant/components/traccar/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/traccar/translations/no.json b/homeassistant/components/traccar/translations/no.json index e2051be22b626..1e16d41880e26 100644 --- a/homeassistant/components/traccar/translations/no.json +++ b/homeassistant/components/traccar/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/traccar/translations/pl.json b/homeassistant/components/traccar/translations/pl.json index 619f6e571928b..0c0cb4249d1d2 100644 --- a/homeassistant/components/traccar/translations/pl.json +++ b/homeassistant/components/traccar/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/traccar/translations/pt-BR.json b/homeassistant/components/traccar/translations/pt-BR.json index eaaa5717709ff..1fc86514bd1f0 100644 --- a/homeassistant/components/traccar/translations/pt-BR.json +++ b/homeassistant/components/traccar/translations/pt-BR.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { - "default": "Para enviar eventos ao Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Traccar. \n\n Use o seguinte URL: ` {webhook_url} ` \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para mais detalhes." + "default": "Para enviar eventos ao Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Traccar. \n\n Use o seguinte URL: ` {webhook_url} ` \n\n Veja [a documenta\u00e7\u00e3o]({docs_url}) para mais detalhes." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/ru.json b/homeassistant/components/traccar/translations/ru.json index 7db0d6f01cf26..5c1a058e5146e 100644 --- a/homeassistant/components/traccar/translations/ru.json +++ b/homeassistant/components/traccar/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index c16dc8c09d2de..e0657d5fddf2a 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/traccar/translations/uk.json b/homeassistant/components/traccar/translations/uk.json index 5bfb1714a7925..4d400941016a9 100644 --- a/homeassistant/components/traccar/translations/uk.json +++ b/homeassistant/components/traccar/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "cloud_not_connected": "\u041d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant Cloud.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/traccar/translations/zh-Hans.json b/homeassistant/components/traccar/translations/zh-Hans.json new file mode 100644 index 0000000000000..d99562320c867 --- /dev/null +++ b/homeassistant/components/traccar/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json index ee7c75d84086f..aa4a250041ea0 100644 --- a/homeassistant/components/traccar/translations/zh-Hant.json +++ b/homeassistant/components/traccar/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index b388703e6bd58..a73c8390ad89f 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -11,5 +11,6 @@ "@zhulik", "@bieniu" ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["aiotractive"] } diff --git a/homeassistant/components/tractive/translations/el.json b/homeassistant/components/tractive/translations/el.json index 15ba157f55ca2..b76849ed2953e 100644 --- a/homeassistant/components/tractive/translations/el.json +++ b/homeassistant/components/tractive/translations/el.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "reauth_failed_existing": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac." + "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", + "reauth_failed_existing": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac.", + "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_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": { "user": { "data": { - "email": "\u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf" + "email": "\u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/tractive/translations/pt-BR.json b/homeassistant/components/tractive/translations/pt-BR.json new file mode 100644 index 0000000000000..d33bdf4f9aa1c --- /dev/null +++ b/homeassistant/components/tractive/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_failed_existing": "N\u00e3o foi poss\u00edvel atualizar a entrada de configura\u00e7\u00e3o. Remova a integra\u00e7\u00e3o e configure-a novamente.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.el.json b/homeassistant/components/tractive/translations/sensor.el.json new file mode 100644 index 0000000000000..2ba41d15fe813 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.el.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "\u03a7\u03c9\u03c1\u03af\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac", + "operational": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc", + "system_shutdown_user": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2", + "system_startup": "\u0395\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sensor.pt-BR.json b/homeassistant/components/tractive/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..4d3efb6fa6772 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.pt-BR.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "N\u00e3o relatando", + "operational": "Operacional", + "system_shutdown_user": "Usu\u00e1rio de desligamento do sistema", + "system_startup": "Inicializa\u00e7\u00e3o do sistema" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/sk.json b/homeassistant/components/tractive/translations/sk.json new file mode 100644 index 0000000000000..f8b6dfeea813e --- /dev/null +++ b/homeassistant/components/tractive/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 952ee54a0d8c1..1971b14b2beb8 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -5,17 +5,24 @@ import logging from typing import Any -from pytradfri import Gateway, PytradfriError, RequestError +from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory +from pytradfri.command import Command +from pytradfri.device import Device +from pytradfri.group import Group import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -28,15 +35,20 @@ CONF_IDENTITY, CONF_IMPORT_GROUPS, CONF_KEY, + COORDINATOR, + COORDINATOR_LIST, DEFAULT_ALLOW_TRADFRI_GROUPS, - DEVICES, DOMAIN, - GROUPS, + GROUPS_LIST, KEY_API, PLATFORMS, SIGNAL_GW, TIMEOUT_API, ) +from .coordinator import ( + TradfriDeviceDataUpdateCoordinator, + TradfriGroupDataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) @@ -46,12 +58,18 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional( - CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS - ): cv.boolean, - } + vol.All( + cv.deprecated(CONF_HOST), + cv.deprecated( + CONF_ALLOW_TRADFRI_GROUPS, + ), + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional( + CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS + ): cv.boolean, + }, + ), ) }, extra=vol.ALLOW_EXTRA, @@ -84,9 +102,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, +) -> bool: """Create a gateway.""" - # host, identity, key, allow_tradfri_groups tradfri_data: dict[str, Any] = {} hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data listeners = tradfri_data[LISTENERS] = [] @@ -96,31 +116,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: psk_id=entry.data[CONF_IDENTITY], psk=entry.data[CONF_KEY], ) + tradfri_data[FACTORY] = factory # Used for async_unload_entry async def on_hass_stop(event: Event) -> None: """Close connection when hass stops.""" await factory.shutdown() + # Setup listeners listeners.append(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)) api = factory.request gateway = Gateway() + groups: list[Group] = [] try: gateway_info = await api(gateway.get_gateway_info(), timeout=TIMEOUT_API) - devices_commands = await api(gateway.get_devices(), timeout=TIMEOUT_API) - devices = await api(devices_commands, timeout=TIMEOUT_API) - groups_commands = await api(gateway.get_groups(), timeout=TIMEOUT_API) - groups = await api(groups_commands, timeout=TIMEOUT_API) - except PytradfriError as exc: + devices_commands: Command = await api( + gateway.get_devices(), timeout=TIMEOUT_API + ) + devices: list[Device] = await api(devices_commands, timeout=TIMEOUT_API) + + if entry.data[CONF_IMPORT_GROUPS]: + # Note: we should update this page when deprecating: + # https://www.home-assistant.io/integrations/tradfri/ + _LOGGER.warning( + "Importing of Tradfri groups has been deprecated due to stability issues " + "and will be removed in Home Assistant core 2022.4" + ) + # No need to load groups if the user hasn't requested it + groups_commands: Command = await api( + gateway.get_groups(), timeout=TIMEOUT_API + ) + groups = await api(groups_commands, timeout=TIMEOUT_API) + + except RequestError as exc: await factory.shutdown() raise ConfigEntryNotReady from exc - tradfri_data[KEY_API] = api - tradfri_data[FACTORY] = factory - tradfri_data[DEVICES] = devices - tradfri_data[GROUPS] = groups - dev_reg = await hass.helpers.device_registry.async_get_registry() dev_reg.async_get_or_create( config_entry_id=entry.entry_id, @@ -133,7 +165,40 @@ async def on_hass_stop(event: Event) -> None: sw_version=gateway_info.firmware_version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + remove_stale_devices(hass, entry, devices) + + # Setup the device coordinators + coordinator_data = { + CONF_GATEWAY_ID: gateway, + KEY_API: api, + COORDINATOR_LIST: [], + GROUPS_LIST: [], + } + + for device in devices: + coordinator = TradfriDeviceDataUpdateCoordinator( + hass=hass, api=api, device=device + ) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_GW, coordinator.set_hub_available) + ) + coordinator_data[COORDINATOR_LIST].append(coordinator) + + for group in groups: + group_coordinator = TradfriGroupDataUpdateCoordinator( + hass=hass, api=api, group=group + ) + await group_coordinator.async_config_entry_first_refresh() + entry.async_on_unload( + async_dispatcher_connect( + hass, SIGNAL_GW, group_coordinator.set_hub_available + ) + ) + coordinator_data[GROUPS_LIST].append(group_coordinator) + + tradfri_data[COORDINATOR] = coordinator_data async def async_keep_alive(now: datetime) -> None: if hass.is_stopping: @@ -152,6 +217,8 @@ async def async_keep_alive(now: datetime) -> None: async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60)) ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True @@ -167,3 +234,45 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: listener() return unload_ok + + +@callback +def remove_stale_devices( + hass: HomeAssistant, config_entry: ConfigEntry, devices: list[Device] +) -> None: + """Remove stale devices from device registry.""" + device_registry = dr.async_get(hass) + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + all_device_ids = {device.id for device in devices} + + for device_entry in device_entries: + device_id: str | None = None + gateway_id: str | None = None + + for identifier in device_entry.identifiers: + if identifier[0] != DOMAIN: + continue + + _id = identifier[1] + + # Identify gateway device. + if _id == config_entry.data[CONF_GATEWAY_ID]: + gateway_id = _id + break + + device_id = _id + break + + if gateway_id is not None: + # Do not remove gateway device entry. + continue + + if device_id is None or device_id not in all_device_ids: + # If device_id is None an invalid device entry was found for this config entry. + # If the device_id is not in existing device ids it's a stale device entry. + # Remove config entry from this device entry in either case. + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=config_entry.entry_id + ) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 34ad7b792b9c2..a2bd28e386840 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -1,29 +1,22 @@ """Base class for IKEA TRADFRI.""" from __future__ import annotations +from abc import abstractmethod from collections.abc import Callable from functools import wraps import logging -from typing import Any +from typing import Any, cast from pytradfri.command import Command from pytradfri.device import Device -from pytradfri.device.air_purifier import AirPurifier -from pytradfri.device.air_purifier_control import AirPurifierControl -from pytradfri.device.blind import Blind -from pytradfri.device.blind_control import BlindControl -from pytradfri.device.light import Light -from pytradfri.device.light_control import LightControl -from pytradfri.device.signal_repeater_control import SignalRepeaterControl -from pytradfri.device.socket import Socket -from pytradfri.device.socket_control import SocketControl -from pytradfri.error import PytradfriError +from pytradfri.error import RequestError from homeassistant.core import 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.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SIGNAL_GW +from .const import DOMAIN +from .coordinator import TradfriDeviceDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -38,108 +31,50 @@ async def wrapper(command: Command | list[Command]) -> None: """Decorate api call.""" try: await func(command) - except PytradfriError as err: + except RequestError as err: _LOGGER.error("Unable to execute command %s: %s", command, err) return wrapper -class TradfriBaseClass(Entity): - """Base class for IKEA TRADFRI. +class TradfriBaseEntity(CoordinatorEntity): + """Base Tradfri device.""" - All devices and groups should ultimately inherit from this class. - """ - - _attr_should_poll = False + coordinator: TradfriDeviceDataUpdateCoordinator def __init__( self, - device: Device, - api: Callable[[Command | list[Command]], Any], + device_coordinator: TradfriDeviceDataUpdateCoordinator, gateway_id: str, + api: Callable[[Command | list[Command]], Any], ) -> None: """Initialize a device.""" - self._api = handle_error(api) - self._attr_name = device.name - self._device: Device = device - self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( - None - ) - self._device_data: Socket | Light | Blind | AirPurifier | None = None + super().__init__(device_coordinator) + self._gateway_id = gateway_id - async def _async_run_observe(self, cmd: Command) -> None: - """Run observe in a coroutine.""" - try: - await self._api(cmd) - except PytradfriError as err: - self._attr_available = False - self.async_write_ha_state() - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() + self._device: Device = device_coordinator.data - @callback - def _async_start_observe(self, exc: Exception | None = None) -> None: - """Start observation of device.""" - if exc: - self._attr_available = False - self.async_write_ha_state() - _LOGGER.warning("Observation failed for %s", self._attr_name, exc_info=exc) - cmd = self._device.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._async_run_observe(cmd)) + self._device_id = self._device.id + self._api = handle_error(api) + self._attr_name = self._device.name - async def async_added_to_hass(self) -> None: - """Start thread when added to hass.""" - self._async_start_observe() + self._attr_unique_id = f"{self._gateway_id}-{self._device.id}" + @abstractmethod @callback - def _observe_update(self, device: Device) -> None: - """Receive new state data for this device.""" - self._refresh(device) - - def _refresh(self, device: Device, write_ha: bool = True) -> None: - """Refresh the device data.""" - self._device = device - self._attr_name = device.name - if write_ha: - self.async_write_ha_state() - - -class TradfriBaseDevice(TradfriBaseClass): - """Base class for a TRADFRI device. - - All devices should inherit from this class. - """ - - def __init__( - self, - device: Device, - api: Callable[[Command | list[Command]], Any], - gateway_id: str, - ) -> None: - """Initialize a device.""" - self._attr_available = device.reachable - self._hub_available = True - super().__init__(device, api, gateway_id) - - async def async_added_to_hass(self) -> None: - """Start thread when added to hass.""" - # Only devices shall receive SIGNAL_GW - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_GW, self.set_hub_available) - ) - await super().async_added_to_hass() + def _refresh(self) -> None: + """Refresh device data.""" @callback - def set_hub_available(self, available: bool) -> None: - """Set status of hub.""" - if available != self._hub_available: - self._hub_available = available - self._refresh(self._device) + def _handle_coordinator_update(self) -> None: + """ + Handle updated data from the coordinator. + + Tests fails without this method. + """ + self._refresh() + super()._handle_coordinator_update() @property def device_info(self) -> DeviceInfo: @@ -149,15 +84,12 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self._device.id)}, manufacturer=info.manufacturer, model=info.model_number, - name=self._attr_name, + name=self._device.name, sw_version=info.firmware_version, via_device=(DOMAIN, self._gateway_id), ) - def _refresh(self, device: Device, write_ha: bool = True) -> None: - """Refresh the device data.""" - # The base class _refresh cannot be used, because - # there are devices (group) that do not have .reachable - # so set _attr_available here and let the base class do the rest. - self._attr_available = device.reachable and self._hub_available - super()._refresh(device, write_ha) + @property + def available(self) -> bool: + """Return if entity is available.""" + return cast(bool, self._device.reachable) and super().available diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 487eb0dae1387..3d68ebbaee0c2 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,4 +1,6 @@ """Consts used by Tradfri.""" +from typing import Final + from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import CONF_HOST, @@ -37,3 +39,11 @@ ] TIMEOUT_API = 30 ATTR_MAX_FAN_STEPS = 49 + +SCAN_INTERVAL = 60 # Interval for updating the coordinator + +COORDINATOR = "coordinator" +COORDINATOR_LIST = "coordinator_list" +GROUPS_LIST = "groups_list" + +ATTR_FILTER_LIFE_REMAINING: Final = "filter_life_remaining" diff --git a/homeassistant/components/tradfri/coordinator.py b/homeassistant/components/tradfri/coordinator.py new file mode 100644 index 0000000000000..039ff34c9f74a --- /dev/null +++ b/homeassistant/components/tradfri/coordinator.py @@ -0,0 +1,141 @@ +"""Tradfri DataUpdateCoordinator.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import logging +from typing import Any + +from pytradfri.command import Command +from pytradfri.device import Device +from pytradfri.error import RequestError +from pytradfri.group import Group + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]): + """Coordinator to manage data for a specific Tradfri device.""" + + def __init__( + self, + hass: HomeAssistant, + *, + api: Callable[[Command | list[Command]], Any], + device: Device, + ) -> None: + """Initialize device coordinator.""" + self.api = api + self.device = device + self._exception: Exception | None = None + + super().__init__( + hass, + _LOGGER, + name=f"Update coordinator for {device}", + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + async def set_hub_available(self, available: bool) -> None: + """Set status of hub.""" + if available != self.last_update_success: + if not available: + self.last_update_success = False + await self.async_request_refresh() + + @callback + def _observe_update(self, device: Device) -> None: + """Update the coordinator for a device when a change is detected.""" + self.async_set_updated_data(data=device) + + @callback + def _exception_callback(self, exc: Exception) -> None: + """Schedule handling exception..""" + self.hass.async_create_task(self._handle_exception(exc)) + + async def _handle_exception(self, exc: Exception) -> None: + """Handle observe exceptions in a coroutine.""" + # Store exception so that it gets raised in _async_update_data + self._exception = exc + + _LOGGER.debug( + "Observation failed for %s, trying again", self.device, exc_info=exc + ) + # Change interval so we get a swift refresh + self.update_interval = timedelta(seconds=5) + await self.async_request_refresh() + + async def _async_update_data(self) -> Device: + """Fetch data from the gateway for a specific device.""" + try: + if self._exception: + exc = self._exception + self._exception = None # Clear stored exception + raise exc # pylint: disable-msg=raising-bad-type + except RequestError as err: + raise UpdateFailed(f"Error communicating with API: {err}.") from err + + if not self.data or not self.last_update_success: # Start subscription + try: + cmd = self.device.observe( + callback=self._observe_update, + err_callback=self._exception_callback, + duration=0, + ) + await self.api(cmd) + except RequestError as err: + raise UpdateFailed(f"Error communicating with API: {err}.") from err + + # Reset update interval + self.update_interval = timedelta(seconds=SCAN_INTERVAL) + + return self.device + + +class TradfriGroupDataUpdateCoordinator(DataUpdateCoordinator[Group]): + """Coordinator to manage data for a specific Tradfri group.""" + + def __init__( + self, + hass: HomeAssistant, + *, + api: Callable[[Command | list[Command]], Any], + group: Group, + ) -> None: + """Initialize group coordinator.""" + self.api = api + self.group = group + self._exception: Exception | None = None + + super().__init__( + hass, + _LOGGER, + name=f"Update coordinator for {group}", + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + async def set_hub_available(self, available: bool) -> None: + """Set status of hub.""" + if available != self.last_update_success: + if not available: + self.last_update_success = False + await self.async_request_refresh() + + async def _async_update_data(self) -> Group: + """Fetch data from the gateway for a specific group.""" + self.update_interval = timedelta(seconds=SCAN_INTERVAL) # Reset update interval + cmd = self.group.update() + try: + await self.api(cmd) + except RequestError as exc: + self.update_interval = timedelta( + seconds=5 + ) # Change interval so we get a swift refresh + raise UpdateFailed("Unable to update group coordinator") from exc + + return self.group diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 554650f900547..d4c3063f35d0a 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -11,8 +11,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .base_class import TradfriBaseDevice -from .const import ATTR_MODEL, CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API +from .base_class import TradfriBaseEntity +from .const import ( + ATTR_MODEL, + CONF_GATEWAY_ID, + COORDINATOR, + COORDINATOR_LIST, + DOMAIN, + KEY_API, +) +from .coordinator import TradfriDeviceDataUpdateCoordinator async def async_setup_entry( @@ -22,28 +30,42 @@ async def async_setup_entry( ) -> None: """Load Tradfri covers based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - tradfri_data = hass.data[DOMAIN][config_entry.entry_id] - api = tradfri_data[KEY_API] - devices = tradfri_data[DEVICES] + coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + api = coordinator_data[KEY_API] async_add_entities( - TradfriCover(dev, api, gateway_id) for dev in devices if dev.has_blind_control + TradfriCover( + device_coordinator, + api, + gateway_id, + ) + for device_coordinator in coordinator_data[COORDINATOR_LIST] + if device_coordinator.device.has_blind_control ) -class TradfriCover(TradfriBaseDevice, CoverEntity): +class TradfriCover(TradfriBaseEntity, CoverEntity): """The platform class required by Home Assistant.""" def __init__( self, - device: Command, + device_coordinator: TradfriDeviceDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, ) -> None: - """Initialize a cover.""" - self._attr_unique_id = f"{gateway_id}-{device.id}" - super().__init__(device, api, gateway_id) - self._refresh(device, write_ha=False) + """Initialize a switch.""" + super().__init__( + device_coordinator=device_coordinator, + api=api, + gateway_id=gateway_id, + ) + + self._device_control = self._device.blind_control + self._device_data = self._device_control.blinds[0] + + def _refresh(self) -> None: + """Refresh the device.""" + self._device_data = self.coordinator.data.blind_control.blinds[0] @property def extra_state_attributes(self) -> dict[str, str] | None: @@ -88,11 +110,3 @@ async def async_stop_cover(self, **kwargs: Any) -> None: def is_closed(self) -> bool: """Return if the cover is closed or not.""" return self.current_cover_position == 0 - - def _refresh(self, device: Command, write_ha: bool = True) -> None: - """Refresh the cover data.""" - # Caching of BlindControl and cover object - self._device = device - self._device_control = device.blind_control - self._device_data = device.blind_control.blinds[0] - super()._refresh(device, write_ha=write_ha) diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py new file mode 100644 index 0000000000000..81f20c4a46a24 --- /dev/null +++ b/homeassistant/components/tradfri/diagnostics.py @@ -0,0 +1,36 @@ +"""Diagnostics support for IKEA Tradfri.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, GROUPS_LIST + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict: + """Return diagnostics the Tradfri platform.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + coordinator_data = entry_data[COORDINATOR] + + device_registry = dr.async_get(hass) + device = cast( + dr.DeviceEntry, + device_registry.async_get_device( + identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])} + ), + ) + + device_data: list = [] + for coordinator in coordinator_data[COORDINATOR_LIST]: + device_data.append(coordinator.device.device_info.model_number) + + return { + "gateway_version": device.sw_version, + "device_data": sorted(device_data), + "no_of_groups": len(coordinator_data[GROUPS_LIST]), + } diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 7b64e883c44a5..36e1c8b08ad88 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -16,15 +16,17 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .base_class import TradfriBaseDevice +from .base_class import TradfriBaseEntity from .const import ( ATTR_AUTO, ATTR_MAX_FAN_STEPS, CONF_GATEWAY_ID, - DEVICES, + COORDINATOR, + COORDINATOR_LIST, DOMAIN, KEY_API, ) +from .coordinator import TradfriDeviceDataUpdateCoordinator def _from_fan_percentage(percentage: int) -> int: @@ -44,30 +46,42 @@ async def async_setup_entry( ) -> None: """Load Tradfri switches based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - tradfri_data = hass.data[DOMAIN][config_entry.entry_id] - api = tradfri_data[KEY_API] - devices = tradfri_data[DEVICES] + coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + api = coordinator_data[KEY_API] async_add_entities( - TradfriAirPurifierFan(dev, api, gateway_id) - for dev in devices - if dev.has_air_purifier_control + TradfriAirPurifierFan( + device_coordinator, + api, + gateway_id, + ) + for device_coordinator in coordinator_data[COORDINATOR_LIST] + if device_coordinator.device.has_air_purifier_control ) -class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): +class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): """The platform class required by Home Assistant.""" def __init__( self, - device: Command, + device_coordinator: TradfriDeviceDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, ) -> None: """Initialize a switch.""" - super().__init__(device, api, gateway_id) - self._attr_unique_id = f"{gateway_id}-{device.id}" - self._refresh(device, write_ha=False) + super().__init__( + device_coordinator=device_coordinator, + api=api, + gateway_id=gateway_id, + ) + + self._device_control = self._device.air_purifier_control + self._device_data = self._device_control.air_purifiers[0] + + def _refresh(self) -> None: + """Refresh the device.""" + self._device_data = self.coordinator.data.air_purifier_control.air_purifiers[0] @property def supported_features(self) -> int: @@ -168,10 +182,3 @@ async def async_turn_off(self, **kwargs: Any) -> None: if not self._device_control: return await self._api(self._device_control.turn_off()) - - def _refresh(self, device: Command, write_ha: bool = True) -> None: - """Refresh the purifier data.""" - # Caching of air purifier control and purifier object - self._device_control = device.air_purifier_control - self._device_data = device.air_purifier_control.air_purifiers[0] - super()._refresh(device, write_ha=write_ha) diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index ca078a37e813b..9b6ad3e9f0676 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -5,6 +5,7 @@ from typing import Any, cast from pytradfri.command import Command +from pytradfri.group import Group from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -19,9 +20,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util -from .base_class import TradfriBaseClass, TradfriBaseDevice +from .base_class import TradfriBaseEntity from .const import ( ATTR_DIMMER, ATTR_HUE, @@ -29,13 +31,18 @@ ATTR_TRANSITION_TIME, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, - DEVICES, + COORDINATOR, + COORDINATOR_LIST, DOMAIN, - GROUPS, + GROUPS_LIST, KEY_API, SUPPORTED_GROUP_FEATURES, SUPPORTED_LIGHT_FEATURES, ) +from .coordinator import ( + TradfriDeviceDataUpdateCoordinator, + TradfriGroupDataUpdateCoordinator, +) async def async_setup_entry( @@ -45,56 +52,66 @@ async def async_setup_entry( ) -> None: """Load Tradfri lights based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - tradfri_data = hass.data[DOMAIN][config_entry.entry_id] - api = tradfri_data[KEY_API] - devices = tradfri_data[DEVICES] - - entities: list[TradfriBaseClass] = [ - TradfriLight(dev, api, gateway_id) for dev in devices if dev.has_light_control + coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + api = coordinator_data[KEY_API] + + entities: list = [ + TradfriLight( + device_coordinator, + api, + gateway_id, + ) + for device_coordinator in coordinator_data[COORDINATOR_LIST] + if device_coordinator.device.has_light_control ] - if config_entry.data[CONF_IMPORT_GROUPS] and (groups := tradfri_data[GROUPS]): - entities.extend([TradfriGroup(group, api, gateway_id) for group in groups]) + + if config_entry.data[CONF_IMPORT_GROUPS] and ( + group_coordinators := coordinator_data[GROUPS_LIST] + ): + entities.extend( + [ + TradfriGroup(group_coordinator, api, gateway_id) + for group_coordinator in group_coordinators + ] + ) + async_add_entities(entities) -class TradfriGroup(TradfriBaseClass, LightEntity): +class TradfriGroup(CoordinatorEntity, LightEntity): """The platform class for light groups required by hass.""" _attr_supported_features = SUPPORTED_GROUP_FEATURES def __init__( self, - device: Command, + group_coordinator: TradfriGroupDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, ) -> None: """Initialize a Group.""" - super().__init__(device, api, gateway_id) - - self._attr_unique_id = f"group-{gateway_id}-{device.id}" - self._attr_should_poll = True - self._refresh(device, write_ha=False) + super().__init__(coordinator=group_coordinator) - async def async_update(self) -> None: - """Fetch new state data for the group. + self._group: Group = self.coordinator.data - This method is required for groups to update properly. - """ - await self._api(self._device.update()) + self._api = api + self._attr_unique_id = f"group-{gateway_id}-{self._group.id}" @property def is_on(self) -> bool: """Return true if group lights are on.""" - return cast(bool, self._device.state) + return cast(bool, self._group.state) @property def brightness(self) -> int | None: """Return the brightness of the group lights.""" - return cast(int, self._device.dimmer) + return cast(int, self._group.dimmer) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the group lights to turn off.""" - await self._api(self._device.set_state(0)) + await self._api(self._group.set_state(0)) + + await self.coordinator.async_request_refresh() async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the group lights to turn on, or dim.""" @@ -106,39 +123,53 @@ async def async_turn_on(self, **kwargs: Any) -> None: if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - await self._api(self._device.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) + await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - await self._api(self._device.set_state(1)) + await self._api(self._group.set_state(1)) + await self.coordinator.async_request_refresh() -class TradfriLight(TradfriBaseDevice, LightEntity): + +class TradfriLight(TradfriBaseEntity, LightEntity): """The platform class required by Home Assistant.""" def __init__( self, - device: Command, + device_coordinator: TradfriDeviceDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, ) -> None: """Initialize a Light.""" - super().__init__(device, api, gateway_id) - self._attr_unique_id = f"light-{gateway_id}-{device.id}" + super().__init__( + device_coordinator=device_coordinator, + api=api, + gateway_id=gateway_id, + ) + + self._device_control = self._device.light_control + self._device_data = self._device_control.lights[0] + + self._attr_unique_id = f"light-{gateway_id}-{self._device_id}" self._hs_color = None # Calculate supported features _features = SUPPORTED_LIGHT_FEATURES - if device.light_control.can_set_dimmer: + if self._device.light_control.can_set_dimmer: _features |= SUPPORT_BRIGHTNESS - if device.light_control.can_set_color: + if self._device.light_control.can_set_color: _features |= SUPPORT_COLOR | SUPPORT_COLOR_TEMP - if device.light_control.can_set_temp: + if self._device.light_control.can_set_temp: _features |= SUPPORT_COLOR_TEMP self._attr_supported_features = _features - self._refresh(device, write_ha=False) + if self._device_control: self._attr_min_mireds = self._device_control.min_mireds self._attr_max_mireds = self._device_control.max_mireds + def _refresh(self) -> None: + """Refresh the device.""" + self._device_data = self.coordinator.data.light_control.lights[0] + @property def is_on(self) -> bool: """Return true if light is on.""" @@ -268,10 +299,3 @@ async def async_turn_on(self, **kwargs: Any) -> None: await self._api(temp_command) if command is not None: await self._api(command) - - def _refresh(self, device: Command, write_ha: bool = True) -> None: - """Refresh the light data.""" - # Caching of LightControl and light object - self._device_control = device.light_control - self._device_data = device.light_control.lights[0] - super()._refresh(device, write_ha=write_ha) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 1ac82d0b84cc6..ae4eb460c6cf6 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,10 +3,11 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==8.0.1"], + "requirements": ["pytradfri[async]==9.0.0"], "homekit": { "models": ["TRADFRI"] }, "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pytradfri"] } diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 5da3179c507d2..1654b78012460 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -2,18 +2,132 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass +import logging from typing import Any, cast from pytradfri.command import Command +from pytradfri.device import Device -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + PERCENTAGE, + TIME_HOURS, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API +from .base_class import TradfriBaseEntity +from .const import ( + ATTR_FILTER_LIFE_REMAINING, + CONF_GATEWAY_ID, + COORDINATOR, + COORDINATOR_LIST, + DOMAIN, + KEY_API, +) +from .coordinator import TradfriDeviceDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class TradfriSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value: Callable[[Device], Any | None] + + +@dataclass +class TradfriSensorEntityDescription( + SensorEntityDescription, + TradfriSensorEntityDescriptionMixin, +): + """Class describing Tradfri sensor entities.""" + + +def _get_air_quality(device: Device) -> int | None: + """Fetch the air quality value.""" + if ( + device.air_purifier_control.air_purifiers[0].air_quality == 65535 + ): # The sensor returns 65535 if the fan is turned off + return None + + return cast(int, device.air_purifier_control.air_purifiers[0].air_quality) + + +def _get_filter_time_left(device: Device) -> int: + """Fetch the filter's remaining life (in hours).""" + return round( + device.air_purifier_control.air_purifiers[0].filter_lifetime_remaining / 60 + ) + + +SENSOR_DESCRIPTIONS_BATTERY: tuple[TradfriSensorEntityDescription, ...] = ( + TradfriSensorEntityDescription( + key="battery_level", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + value=lambda device: cast(int, device.device_info.battery_level), + ), +) + + +SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( + TradfriSensorEntityDescription( + key="aqi", + name="air quality", + device_class=SensorDeviceClass.AQI, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + value=_get_air_quality, + ), + TradfriSensorEntityDescription( + key=ATTR_FILTER_LIFE_REMAINING, + name="filter time left", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_HOURS, + icon="mdi:clock-outline", + value=_get_filter_time_left, + ), +) + + +@callback +def _migrate_old_unique_ids(hass: HomeAssistant, old_unique_id: str, key: str) -> None: + """Migrate unique IDs to the new format.""" + ent_reg = entity_registry.async_get(hass) + + entity_id = ent_reg.async_get_entity_id(Platform.SENSOR, DOMAIN, old_unique_id) + + if entity_id is None: + return + + new_unique_id = f"{old_unique_id}-{key}" + + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + _LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + return + + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) async def async_setup_entry( @@ -23,42 +137,72 @@ async def async_setup_entry( ) -> None: """Set up a Tradfri config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - tradfri_data = hass.data[DOMAIN][config_entry.entry_id] - api = tradfri_data[KEY_API] - devices = tradfri_data[DEVICES] + coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + api = coordinator_data[KEY_API] + + entities: list[TradfriSensor] = [] - async_add_entities( - TradfriSensor(dev, api, gateway_id) - for dev in devices + for device_coordinator in coordinator_data[COORDINATOR_LIST]: if ( - not dev.has_light_control - and not dev.has_socket_control - and not dev.has_blind_control - and not dev.has_signal_repeater_control - and not dev.has_air_purifier_control - ) - ) + not device_coordinator.device.has_light_control + and not device_coordinator.device.has_socket_control + and not device_coordinator.device.has_signal_repeater_control + and not device_coordinator.device.has_air_purifier_control + ): + descriptions = SENSOR_DESCRIPTIONS_BATTERY + elif device_coordinator.device.has_air_purifier_control: + descriptions = SENSOR_DESCRIPTIONS_FAN + else: + continue + for description in descriptions: + # Added in Home assistant 2022.3 + _migrate_old_unique_ids( + hass=hass, + old_unique_id=f"{gateway_id}-{device_coordinator.device.id}", + key=description.key, + ) -class TradfriSensor(TradfriBaseDevice, SensorEntity): + entities.append( + TradfriSensor( + device_coordinator, + api, + gateway_id, + description=description, + ) + ) + + async_add_entities(entities) + + +class TradfriSensor(TradfriBaseEntity, SensorEntity): """The platform class required by Home Assistant.""" - _attr_device_class = SensorDeviceClass.BATTERY - _attr_native_unit_of_measurement = PERCENTAGE + entity_description: TradfriSensorEntityDescription def __init__( self, - device: Command, + device_coordinator: TradfriDeviceDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, + description: TradfriSensorEntityDescription, ) -> None: - """Initialize the device.""" - super().__init__(device, api, gateway_id) - self._attr_unique_id = f"{gateway_id}-{device.id}" - - @property - def native_value(self) -> int | None: - """Return the current state of the device.""" - if not self._device: - return None - return cast(int, self._device.device_info.battery_level) + """Initialize a Tradfri sensor.""" + super().__init__( + device_coordinator=device_coordinator, + api=api, + gateway_id=gateway_id, + ) + + self.entity_description = description + + self._attr_unique_id = f"{self._attr_unique_id}-{description.key}" + + if description.name: + self._attr_name = f"{self._attr_name}: {description.name}" + + self._refresh() # Set initial state + + def _refresh(self) -> None: + """Refresh the device.""" + self._attr_native_value = self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index f8950d247204f..e0e2467ca4bf3 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -11,8 +11,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API +from .base_class import TradfriBaseEntity +from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN, KEY_API +from .coordinator import TradfriDeviceDataUpdateCoordinator async def async_setup_entry( @@ -22,35 +23,42 @@ async def async_setup_entry( ) -> None: """Load Tradfri switches based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - tradfri_data = hass.data[DOMAIN][config_entry.entry_id] - api = tradfri_data[KEY_API] - devices = tradfri_data[DEVICES] + coordinator_data = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + api = coordinator_data[KEY_API] async_add_entities( - TradfriSwitch(dev, api, gateway_id) for dev in devices if dev.has_socket_control + TradfriSwitch( + device_coordinator, + api, + gateway_id, + ) + for device_coordinator in coordinator_data[COORDINATOR_LIST] + if device_coordinator.device.has_socket_control ) -class TradfriSwitch(TradfriBaseDevice, SwitchEntity): +class TradfriSwitch(TradfriBaseEntity, SwitchEntity): """The platform class required by Home Assistant.""" def __init__( self, - device: Command, + device_coordinator: TradfriDeviceDataUpdateCoordinator, api: Callable[[Command | list[Command]], Any], gateway_id: str, ) -> None: """Initialize a switch.""" - super().__init__(device, api, gateway_id) - self._attr_unique_id = f"{gateway_id}-{device.id}" - self._refresh(device, write_ha=False) + super().__init__( + device_coordinator=device_coordinator, + api=api, + gateway_id=gateway_id, + ) - def _refresh(self, device: Command, write_ha: bool = True) -> None: - """Refresh the switch data.""" - # Caching of switch control and switch object - self._device_control = device.socket_control - self._device_data = device.socket_control.sockets[0] - super()._refresh(device, write_ha=write_ha) + self._device_control = self._device.socket_control + self._device_data = self._device_control.sockets[0] + + def _refresh(self) -> None: + """Refresh the device.""" + self._device_data = self.coordinator.data.socket_control.sockets[0] @property def is_on(self) -> bool: diff --git a/homeassistant/components/tradfri/translations/el.json b/homeassistant/components/tradfri/translations/el.json index feee649656d64..8e37ecbf503d3 100644 --- a/homeassistant/components/tradfri/translations/el.json +++ b/homeassistant/components/tradfri/translations/el.json @@ -1,12 +1,19 @@ { "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" + }, "error": { + "cannot_authenticate": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2, \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03c0\u03cd\u03bb\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03ad\u03bd\u03b1\u03bd \u03ac\u03bb\u03bb\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae, \u03cc\u03c0\u03c9\u03c2 \u03c0.\u03c7. \u03c4\u03bf Homekit;", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_key": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03bc\u03b5 \u03c4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af. \u0391\u03bd \u03b1\u03c5\u03c4\u03cc \u03c3\u03c5\u03bc\u03b2\u03b1\u03af\u03bd\u03b5\u03b9 \u03c3\u03c5\u03bd\u03b5\u03c7\u03ce\u03c2, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7.", "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd." }, "step": { "auth": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "security_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2" }, "description": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf \u03c0\u03af\u03c3\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c3\u03b1\u03c2.", diff --git a/homeassistant/components/tradfri/translations/pt-BR.json b/homeassistant/components/tradfri/translations/pt-BR.json index b1c853f5f2b01..5cf3afc282d3b 100644 --- a/homeassistant/components/tradfri/translations/pt-BR.json +++ b/homeassistant/components/tradfri/translations/pt-BR.json @@ -1,18 +1,19 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o de ponte 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" }, "error": { - "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao gateway.", + "cannot_authenticate": "N\u00e3o \u00e9 poss\u00edvel autenticar, o Gateway est\u00e1 emparelhado com outro servidor como, por exemplo, Homekit?", + "cannot_connect": "Falha ao conectar", "invalid_key": "Falha ao registrar-se com a chave fornecida. Se isso continuar acontecendo, tente reiniciar o gateway.", "timeout": "Excedido tempo limite para validar c\u00f3digo" }, "step": { "auth": { "data": { - "host": "Hospedeiro", + "host": "Nome do host", "security_code": "C\u00f3digo de seguran\u00e7a" }, "description": "Voc\u00ea pode encontrar o c\u00f3digo de seguran\u00e7a na parte de tr\u00e1s do seu gateway.", diff --git a/homeassistant/components/tradfri/translations/sk.json b/homeassistant/components/tradfri/translations/sk.json new file mode 100644 index 0000000000000..299acb612fbab --- /dev/null +++ b/homeassistant/components/tradfri/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 36a1d47623e2b..da1d4de6c1316 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": ["pytrafikverket==0.1.6.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pytrafikverket"] } diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 6490468dc03fc..4001856b703cd 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pytrafikverket==0.1.6.2"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pytrafikverket"] } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/el.json b/homeassistant/components/trafikverket_weatherstation/translations/el.json index 28ec7eb54f160..e790e3d3d9796 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/el.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/el.json @@ -1,7 +1,23 @@ { "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" + }, "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_station": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03b5\u03c9\u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1", "more_stations": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03bf\u03af \u03bc\u03b5\u03c4\u03b5\u03c9\u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03bf\u03af \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03bc\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "conditions": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b8\u03ae\u03ba\u03b5\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "station": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2/\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/nb.json b/homeassistant/components/trafikverket_weatherstation/translations/nb.json new file mode 100644 index 0000000000000..19fbf894f8d5d --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/nb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig autentisering" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json b/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json new file mode 100644 index 0000000000000..f73ab8555da71 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_station": "N\u00e3o foi poss\u00edvel encontrar uma esta\u00e7\u00e3o meteorol\u00f3gica com o nome especificado", + "more_stations": "Encontrado v\u00e1rias esta\u00e7\u00f5es meteorol\u00f3gicas com o nome especificado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "conditions": "Condi\u00e7\u00f5es monitoradas", + "name": "Usu\u00e1rio", + "station": "Esta\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/sk.json b/homeassistant/components/trafikverket_weatherstation/translations/sk.json new file mode 100644 index 0000000000000..ff85312780312 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 6b9d8c0aeb165..a824185b13e35 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -330,7 +330,7 @@ def refresh(event_time): ) @staticmethod - async def async_options_updated(hass, entry): + async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Triggered by config entry options updates.""" tm_client = hass.data[DOMAIN][entry.entry_id] tm_client.set_scan_interval(entry.options[CONF_SCAN_INTERVAL]) diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index 1f5843e5e6c88..8f4fabc529d1b 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/transmission", "requirements": ["transmissionrpc==0.11"], "codeowners": ["@engrbm87", "@JPHutchins"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["transmissionrpc"] } diff --git a/homeassistant/components/transmission/translations/cs.json b/homeassistant/components/transmission/translations/cs.json index 8ad9e05106418..c2c0bf5ead01a 100644 --- a/homeassistant/components/transmission/translations/cs.json +++ b/homeassistant/components/transmission/translations/cs.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "name_exists": "Jm\u00e9no already exists" + "name_exists": "Jm\u00e9no ji\u017e existuje" }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index fdb3d8a981d55..4a0701cdaf92c 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -1,10 +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", "name_exists": "\u03a4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" }, "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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2-\u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2" } } @@ -13,6 +25,8 @@ "step": { "init": { "data": { + "limit": "\u038c\u03c1\u03b9\u03bf", + "order": "\u03a3\u03b5\u03b9\u03c1\u03ac", "scan_interval": "\u03a3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf Transmission" diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index fdc42bcf30304..3353884ef3338 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -1,16 +1,17 @@ { "config": { "abort": { - "already_configured": "O host j\u00e1 est\u00e1 configurado." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", - "name_exists": "O nome j\u00e1 existe" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "name_exists": "O Nome j\u00e1 existe" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "name": "Nome", "password": "Senha", "port": "Porta", @@ -24,6 +25,8 @@ "step": { "init": { "data": { + "limit": "Limite", + "order": "Pedido", "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" }, "title": "Configurar op\u00e7\u00f5es para Transmission" diff --git a/homeassistant/components/transmission/translations/sk.json b/homeassistant/components/transmission/translations/sk.json new file mode 100644 index 0000000000000..731004b0ebc32 --- /dev/null +++ b/homeassistant/components/transmission/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie", + "name_exists": "N\u00e1zov u\u017e existuje" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index e6670b0e4f6dd..994fcde1b297a 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/transport_nsw", "requirements": ["PyTransportNSW==0.1.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["TransportNSW"] } diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index c991eecebb2e2..874563745cfb0 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/travisci", "requirements": ["TravisPy==0.3.5"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["travispy"] } diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 515dc25899ad1..5e6629ca2a274 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -9,6 +9,7 @@ import logging import mimetypes import os +from pathlib import Path import re from typing import TYPE_CHECKING, Optional, cast @@ -39,10 +40,11 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.loader import async_get_integration from homeassistant.setup import async_prepare_setup_platform from homeassistant.util.yaml import load_yaml +from .const import DOMAIN + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -69,7 +71,6 @@ DEFAULT_CACHE = True DEFAULT_CACHE_DIR = "tts" DEFAULT_TIME_MEMORY = 300 -DOMAIN = "tts" MEM_CACHE_FILENAME = "filename" MEM_CACHE_VOICE = "voice" @@ -135,12 +136,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.exception("Error on cache init") return False + hass.data[DOMAIN] = tts hass.http.register_view(TextToSpeechView(tts)) hass.http.register_view(TextToSpeechUrlView(tts)) # Load service descriptions from tts/services.yaml - integration = await async_get_integration(hass, DOMAIN) - services_yaml = integration.file_path / "services.yaml" + services_yaml = Path(__file__).parent / "services.yaml" services_dict = cast( dict, await hass.async_add_executor_job(load_yaml, str(services_yaml)) ) @@ -343,7 +344,9 @@ async def async_get_url_path( This method is a coroutine. """ - provider = self.providers[engine] + if (provider := self.providers.get(engine)) is None: + raise HomeAssistantError(f"Provider {engine} not found") + msg_hash = hashlib.sha1(bytes(message, "utf-8")).hexdigest() use_cache = cache if cache is not None else self.use_cache diff --git a/homeassistant/components/tts/const.py b/homeassistant/components/tts/const.py new file mode 100644 index 0000000000000..492e995b87f0b --- /dev/null +++ b/homeassistant/components/tts/const.py @@ -0,0 +1,3 @@ +"""Text-to-speech constants.""" + +DOMAIN = "tts" diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index 8f7d203c215af..f81d112e82538 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["http"], "after_dependencies": ["media_player"], "codeowners": ["@pvizeli"], - "quality_scale": "internal" + "quality_scale": "internal", + "loggers": ["mutagen"] } diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py new file mode 100644 index 0000000000000..48bb43990efbe --- /dev/null +++ b/homeassistant/components/tts/media_source.py @@ -0,0 +1,114 @@ +"""Text-to-speech media source.""" +from __future__ import annotations + +import mimetypes +from typing import TYPE_CHECKING + +from yarl import URL + +from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +if TYPE_CHECKING: + from . import SpeechManager + + +async def async_get_media_source(hass: HomeAssistant) -> TTSMediaSource: + """Set up tts media source.""" + return TTSMediaSource(hass) + + +class TTSMediaSource(MediaSource): + """Provide text-to-speech providers as media sources.""" + + name: str = "Text to Speech" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize TTSMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + parsed = URL(item.identifier) + if "message" not in parsed.query: + raise Unresolvable("No message specified.") + + options = dict(parsed.query) + kwargs = { + "engine": parsed.name, + "message": options.pop("message"), + "language": options.pop("language", None), + "options": options, + } + + manager: SpeechManager = self.hass.data[DOMAIN] + + try: + url = await manager.async_get_url_path(**kwargs) # type: ignore[arg-type] + except HomeAssistantError as err: + raise Unresolvable(str(err)) from err + + mime_type = mimetypes.guess_type(url)[0] or "audio/mpeg" + + return PlayMedia(url, mime_type) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if item.identifier: + provider, _, params = item.identifier.partition("?") + return self._provider_item(provider, params) + + # Root. List providers. + manager: SpeechManager = self.hass.data[DOMAIN] + children = [self._provider_item(provider) for provider in manager.providers] + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_APP, + media_content_type="", + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_APP, + children=children, + ) + + @callback + def _provider_item( + self, provider_domain: str, params: str | None = None + ) -> BrowseMediaSource: + """Return provider item.""" + manager: SpeechManager = self.hass.data[DOMAIN] + if (provider := manager.providers.get(provider_domain)) is None: + raise BrowseError("Unknown provider") + + if params: + params = f"?{params}" + else: + params = "" + + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{provider_domain}{params}", + media_class=MEDIA_CLASS_APP, + media_content_type="provider", + title=provider.name, + thumbnail=f"https://brands.home-assistant.io/_/{provider_domain}/logo.png", + can_play=False, + can_expand=True, + ) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index c6050255901f1..553b3cc95903d 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -41,15 +41,15 @@ def min_scaled(self) -> float: @property def step_scaled(self) -> float: """Return the step scaled.""" - return self.step / (10 ** self.scale) + return self.step / (10**self.scale) def scale_value(self, value: float | int) -> float: """Scale a value.""" - return value * self.step / (10 ** self.scale) + return value * self.step / (10**self.scale) def scale_value_back(self, value: float | int) -> int: """Return raw value for scaled.""" - return int((value * (10 ** self.scale)) / self.step) + return int((value * (10**self.scale)) / self.step) def remap_value_to( self, diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 7142949f0c60f..2c61151bfafb8 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -110,6 +110,14 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): ), TAMPER_BINARY_SENSOR, ), + # Door and Window Controller + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9 + "mc": ( + TuyaBinarySensorEntityDescription( + key=DPCode.DOORCONTACT_STATE, + device_class=BinarySensorDeviceClass.DOOR, + ), + ), # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": ( @@ -194,6 +202,16 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): ), TAMPER_BINARY_SENSOR, ), + # Thermostatic Radiator Valve + # Not documented + "wkf": ( + TuyaBinarySensorEntityDescription( + key=DPCode.WINDOW_STATE, + name="Window", + device_class=BinarySensorDeviceClass.WINDOW, + on_value="opened", + ), + ), # Temperature and Humidity Sensor # https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34 "wsdcg": (TAMPER_BINARY_SENSOR,), diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 700b974e3c9a2..b70f81bc4d584 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -73,12 +73,24 @@ class TuyaClimateEntityDescription( key="qn", switch_only_hvac_mode=HVAC_MODE_HEAT, ), + # Heater + # https://developer.tuya.com/en/docs/iot/categoryrs?id=Kaiuz0nfferyx + "rs": TuyaClimateEntityDescription( + key="rs", + switch_only_hvac_mode=HVAC_MODE_HEAT, + ), # Thermostat # https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9 "wk": TuyaClimateEntityDescription( key="wk", switch_only_hvac_mode=HVAC_MODE_HEAT_COOL, ), + # Thermostatic Radiator Valve + # Not documented + "wkf": TuyaClimateEntityDescription( + key="wkf", + switch_only_hvac_mode=HVAC_MODE_HEAT, + ), } diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 4d7f9d8e16674..e734004065837 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -87,11 +87,18 @@ class TuyaDeviceClass(StrEnum): """Tuya specific device classes, used for translations.""" + AIR_QUALITY = "tuya__air_quality" + CURTAIN_MODE = "tuya__curtain_mode" + CURTAIN_MOTOR_MODE = "tuya__curtain_motor_mode" BASIC_ANTI_FLICKR = "tuya__basic_anti_flickr" BASIC_NIGHTVISION = "tuya__basic_nightvision" + COUNTDOWN = "tuya__countdown" DECIBEL_SENSITIVITY = "tuya__decibel_sensitivity" FAN_ANGLE = "tuya__fan_angle" FINGERBOT_MODE = "tuya__fingerbot_mode" + HUMIDIFIER_SPRAY_MODE = "tuya__humidifier_spray_mode" + HUMIDIFIER_LEVEL = "tuya__humidifier_level" + HUMIDIFIER_MOODLIGHTING = "tuya__humidifier_moodlighting" IPC_WORK_MODE = "tuya__ipc_work_mode" LED_TYPE = "tuya__led_type" LIGHT_MODE = "tuya__light_mode" @@ -130,6 +137,7 @@ class DPCode(StrEnum): https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq """ + AIR_QUALITY = "air_quality" ALARM_SWITCH = "alarm_switch" # Alarm switch ALARM_TIME = "alarm_time" # Alarm time ALARM_VOLUME = "alarm_volume" # Alarm volume @@ -187,7 +195,10 @@ class DPCode(StrEnum): CONTROL = "control" CONTROL_2 = "control_2" CONTROL_3 = "control_3" + CONTROL_BACK = "control_back" + CONTROL_BACK_MODE = "control_back_mode" COUNTDOWN = "countdown" # Countdown + COUNTDOWN_LEFT = "countdown_left" COUNTDOWN_SET = "countdown_set" # Countdown setting CRY_DETECTION_SWITCH = "cry_detection_switch" CUP_NUMBER = "cup_number" # NUmber of cups @@ -202,6 +213,7 @@ class DPCode(StrEnum): DOORCONTACT_STATE_2 = "doorcontact_state_2" DOORCONTACT_STATE_3 = "doorcontact_state_3" DUSTER_CLOTH = "duster_cloth" + ECO2 = "eco2" EDGE_BRUSH = "edge_brush" ELECTRICITY_LEFT = "electricity_left" FAN_BEEP = "fan_beep" # Sound @@ -217,6 +229,7 @@ class DPCode(StrEnum): FAULT = "fault" FEED_REPORT = "feed_report" FEED_STATE = "feed_state" + FILTER = "filter" FILTER_LIFE = "filter" FILTER_RESET = "filter_reset" # Filter (cartridge) reset FLOODLIGHT_LIGHTNESS = "floodlight_lightness" @@ -226,6 +239,7 @@ class DPCode(StrEnum): GAS_SENSOR_STATUS = "gas_sensor_status" GAS_SENSOR_VALUE = "gas_sensor_value" HUMIDIFIER = "humidifier" # Humidification + HUMIDITY = "humidity" # Humidity HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity @@ -234,6 +248,7 @@ class DPCode(StrEnum): LED_TYPE_2 = "led_type_2" LED_TYPE_3 = "led_type_3" LEVEL = "level" + LEVEL_CURRENT = "level_current" LIGHT = "light" # Light LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock @@ -242,6 +257,7 @@ class DPCode(StrEnum): MANUAL_FEED = "manual_feed" MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + MOODLIGHTING = "moodlighting" # Mood light MOTION_RECORD = "motion_record" MOTION_SENSITIVITY = "motion_sensitivity" MOTION_SWITCH = "motion_switch" # Motion switch @@ -249,6 +265,7 @@ class DPCode(StrEnum): MOVEMENT_DETECT_PIC = "movement_detect_pic" MUFFLING = "muffling" # Muffling NEAR_DETECTION = "near_detection" + OPPOSITE = "opposite" PAUSE = "pause" PERCENT_CONTROL = "percent_control" PERCENT_CONTROL_2 = "percent_control_2" @@ -263,6 +280,7 @@ class DPCode(StrEnum): PIR = "pir" # Motion sensor PM1 = "pm1" PM10 = "pm10" + PM25 = "pm25" PM25_STATE = "pm25_state" PM25_VALUE = "pm25_value" POWDER_SET = "powder_set" # Powder @@ -290,6 +308,7 @@ class DPCode(StrEnum): SHOCK_STATE = "shock_state" # Vibration status SIREN_SWITCH = "siren_switch" SITUATION_SET = "situation_set" + SLEEP = "sleep" # Sleep function SLOW_FEED = "slow_feed" SMOKE_SENSOR_STATE = "smoke_sensor_state" SMOKE_SENSOR_STATUS = "smoke_sensor_status" @@ -297,8 +316,10 @@ class DPCode(StrEnum): SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level + SPRAY_MODE = "spray_mode" # Spraying mode START = "start" # Start STATUS = "status" + STERILIZATION = "sterilization" # Sterilization SUCTION = "suction" SWING = "swing" # Swing mode SWITCH = "switch" # Switch @@ -308,6 +329,8 @@ class DPCode(StrEnum): SWITCH_4 = "switch_4" # Switch 4 SWITCH_5 = "switch_5" # Switch 5 SWITCH_6 = "switch_6" # Switch 6 + SWITCH_7 = "switch_7" # Switch 7 + SWITCH_8 = "switch_8" # Switch 8 SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_CHARGE = "switch_charge" SWITCH_CONTROLLER = "switch_controller" @@ -320,6 +343,7 @@ class DPCode(StrEnum): SWITCH_LED_3 = "switch_led_3" SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SAVE_ENERGY = "switch_save_energy" + SWITCH_SOUND = "switch_sound" # Voice switch SWITCH_SPRAY = "switch_spray" # Spraying switch SWITCH_USB1 = "switch_usb1" # USB 1 SWITCH_USB2 = "switch_usb2" # USB 2 @@ -341,6 +365,7 @@ class DPCode(StrEnum): TEMP_VALUE = "temp_value" # Color temperature TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm + TIME_TOTAL = "time_total" TOTAL_CLEAN_AREA = "total_clean_area" TOTAL_CLEAN_COUNT = "total_clean_count" TOTAL_CLEAN_TIME = "total_clean_time" @@ -365,6 +390,8 @@ class DPCode(StrEnum): WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" WET = "wet" # Humidification + WINDOW_CHECK = "window_check" + WINDOW_STATE = "window_state" WINDSPEED = "windspeed" WIRELESS_BATTERYLOCK = "wireless_batterylock" WIRELESS_ELECTRICITY = "wireless_electricity" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 36a7bdf1ef1fb..4c35c850c33ff 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -108,6 +108,13 @@ class TuyaLightEntityDescription(LightEntityDescription): color_temp=(DPCode.TEMP_VALUE_V2, DPCode.TEMP_VALUE), color_data=(DPCode.COLOUR_DATA_V2, DPCode.COLOUR_DATA), ), + # Not documented + # Based on multiple reports: manufacturer customized Dimmer 2 switches + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_1, + name="Light", + brightness=DPCode.BRIGHT_VALUE_1, + ), ), # Ceiling Fan Light # https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v @@ -170,6 +177,28 @@ class TuyaLightEntityDescription(LightEntityDescription): entity_category=EntityCategory.CONFIG, ), ), + # Unknown light product + # Found as VECINO RGBW as provided by diagnostics + # Not documented + "mbd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Unknown product with light capabilities + # Fond in some diffusers, plugs and PIR flood lights + # Not documented + "qjdcz": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), # Heater # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm "qn": ( diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 1b8772a36df9f..24f9324fe5e39 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -19,5 +19,6 @@ { "macaddress": "84E342*" }, { "macaddress": "D4A651*" }, { "macaddress": "D81F12*" } - ] + ], + "loggers": ["tuya_iot"] } diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index d234b6778beb5..d9cde61a2760e 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -250,6 +250,20 @@ icon="mdi:thermometer-lines", ), ), + # Humidifier + # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b + "jsq": ( + NumberEntityDescription( + key=DPCode.TEMP_SET, + name="Temperature", + icon="mdi:thermometer-lines", + ), + NumberEntityDescription( + key=DPCode.TEMP_SET_F, + name="Temperature", + icon="mdi:thermometer-lines", + ), + ), } diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index ce89000acd2e6..d9103b916f461 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -247,12 +247,88 @@ SelectEntityDescription( key=DPCode.COUNTDOWN, name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, - name="Countdown Setting", + name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + ), + ), + # Curtain + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc + "cl": ( + SelectEntityDescription( + key=DPCode.CONTROL_BACK_MODE, + name="Motor Mode", + device_class=TuyaDeviceClass.CURTAIN_MOTOR_MODE, + entity_category=EntityCategory.CONFIG, + icon="mdi:swap-horizontal", + ), + SelectEntityDescription( + key=DPCode.MODE, + name="Mode", + device_class=TuyaDeviceClass.CURTAIN_MODE, + entity_category=EntityCategory.CONFIG, + ), + ), + # Humidifier + # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b + "jsq": ( + SelectEntityDescription( + key=DPCode.SPRAY_MODE, + name="Spray Mode", + device_class=TuyaDeviceClass.HUMIDIFIER_SPRAY_MODE, + entity_category=EntityCategory.CONFIG, + icon="mdi:spray", + ), + SelectEntityDescription( + key=DPCode.LEVEL, + name="Spraying Level", + device_class=TuyaDeviceClass.HUMIDIFIER_LEVEL, + entity_category=EntityCategory.CONFIG, + icon="mdi:spray", + ), + SelectEntityDescription( + key=DPCode.MOODLIGHTING, + name="Moodlighting", + device_class=TuyaDeviceClass.HUMIDIFIER_MOODLIGHTING, + entity_category=EntityCategory.CONFIG, + icon="mdi:lightbulb-multiple", + ), + SelectEntityDescription( + key=DPCode.COUNTDOWN, + name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + ), + SelectEntityDescription( + key=DPCode.COUNTDOWN_SET, + name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + ), + ), + # Air Purifier + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm + "kj": ( + SelectEntityDescription( + key=DPCode.COUNTDOWN, + name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + ), + SelectEntityDescription( + key=DPCode.COUNTDOWN_SET, + name="Countdown", + device_class=TuyaDeviceClass.COUNTDOWN, entity_category=EntityCategory.CONFIG, icon="mdi:timer-cog-outline", ), diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 2799d2b82fcf6..3ee88d2d57be6 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -295,6 +295,9 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), *BATTERY_SENSORS, ), + # Door and Window Controller + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9 + "mc": BATTERY_SENSORS, # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": BATTERY_SENSORS, @@ -447,6 +450,9 @@ class TuyaSensorEntityDescription(SensorEntityDescription): ), *BATTERY_SENSORS, ), + # Thermostatic Radiator Valve + # Not documented + "wkf": BATTERY_SENSORS, # Temperature and Humidity Sensor # https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34 "wsdcg": ( @@ -729,6 +735,115 @@ class TuyaSensorEntityDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, ), ), + # Curtain + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48qy7wkre + "cl": ( + TuyaSensorEntityDescription( + key=DPCode.TIME_TOTAL, + name="Last Operation Duration", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:progress-clock", + ), + ), + # Humidifier + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48qwjz0i3 + "jsq": ( + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_CURRENT, + 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, + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT_F, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.LEVEL_CURRENT, + name="Water Level", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:waves-arrow-up", + ), + ), + # Air Purifier + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r41mn81 + "kj": ( + TuyaSensorEntityDescription( + key=DPCode.FILTER, + name="Filter Utilization", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:ticket-percent-outline", + ), + TuyaSensorEntityDescription( + key=DPCode.PM25, + name="Particulate Matter 2.5 µm", + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:molecule", + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TVOC, + name="Total Volatile Organic Compound", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.ECO2, + name="Concentration of Carbon Dioxide", + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_TIME, + name="Total Operating Time", + icon="mdi:history", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TuyaSensorEntityDescription( + key=DPCode.TOTAL_PM, + name="Total Absorption of Particles", + icon="mdi:texture-box", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TuyaSensorEntityDescription( + key=DPCode.AIR_QUALITY, + name="Air quality", + icon="mdi:air-filter", + device_class=TuyaDeviceClass.AIR_QUALITY, + ), + ), + # Fan + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48quojr54 + "fs": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) @@ -832,6 +947,10 @@ def __init__( self._attr_device_class = None return + # If we still have a device class, we should not use an icon + if self.device_class: + self._attr_icon = None + # Found unit of measurement, use the standardized Unit # Use the target conversion unit (if set) self._attr_native_unit_of_measurement = ( diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json index 48bf653979eda..a765912d03610 100644 --- a/homeassistant/components/tuya/strings.select.json +++ b/homeassistant/components/tuya/strings.select.json @@ -85,6 +85,49 @@ "30": "30°", "60": "60°", "90": "90°" + }, + "tuya__curtain_mode": { + "morning": "Morning", + "night": "Night" + }, + "tuya__curtain_motor_mode": { + "forward": "Forward", + "back": "Back" + }, + "tuya__countdown": { + "cancel": "Cancel", + "1h": "1 hour", + "2h": "2 hours", + "3h": "3 hours", + "4h": "4 hours", + "5h": "5 hours", + "6h": "6 hours" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Health", + "sleep": "Sleep", + "humidity": "Humidity", + "work": "Work" + }, + "tuya__humidifier_level": { + "level_1": "Level 1", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9", + "level_10": "Level 10" + }, + "tuya__humidifier_moodlighting": { + "1": "Mood 1", + "2": "Mood 2", + "3": "Mood 3", + "4": "Mood 4", + "5": "Mood 5" } } } diff --git a/homeassistant/components/tuya/strings.sensor.json b/homeassistant/components/tuya/strings.sensor.json index ff246817f61ea..a11aadba3219a 100644 --- a/homeassistant/components/tuya/strings.sensor.json +++ b/homeassistant/components/tuya/strings.sensor.json @@ -10,6 +10,12 @@ "reserve_3": "Reserve 3", "standby": "Standby", "warm": "Heat preservation" + }, + "tuya__air_quality": { + "great": "Great", + "mild": "Mild", + "good": "Good", + "severe": "Severe" } } } diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 4e36aec5ee519..d978b377cc5f8 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -137,6 +137,16 @@ name="Switch 6", device_class=SwitchDeviceClass.OUTLET, ), + SwitchEntityDescription( + key=DPCode.SWITCH_7, + name="Switch 7", + device_class=SwitchDeviceClass.OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_8, + name="Switch 8", + device_class=SwitchDeviceClass.OUTLET, + ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, name="USB 1", @@ -198,6 +208,12 @@ icon="mdi:water-percent", entity_category=EntityCategory.CONFIG, ), + SwitchEntityDescription( + key=DPCode.UV, + name="UV Sterilization", + icon="mdi:minus-circle-outline", + entity_category=EntityCategory.CONFIG, + ), ), # Air conditioner # https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n @@ -284,6 +300,15 @@ device_class=SwitchDeviceClass.OUTLET, ), ), + # Unknown product with switch capabilities + # Fond in some diffusers, plugs and PIR flood lights + # Not documented + "qjdcz": ( + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch", + ), + ), # Heater # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm "qn": ( @@ -443,6 +468,22 @@ entity_category=EntityCategory.CONFIG, ), ), + # Thermostatic Radiator Valve + # Not documented + "wkf": ( + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.WINDOW_CHECK, + name="Open Window Detection", + icon="mdi:window-open", + entity_category=EntityCategory.CONFIG, + ), + ), # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( @@ -501,6 +542,12 @@ icon="mdi:molecule", entity_category=EntityCategory.CONFIG, ), + SwitchEntityDescription( + key=DPCode.FAN_COOL, + name="Natural Wind", + icon="mdi:weather-windy", + entity_category=EntityCategory.CONFIG, + ), SwitchEntityDescription( key=DPCode.FAN_BEEP, name="Sound", @@ -514,6 +561,44 @@ entity_category=EntityCategory.CONFIG, ), ), + # Curtain + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc + "cl": ( + SwitchEntityDescription( + key=DPCode.CONTROL_BACK, + name="Reverse", + icon="mdi:swap-horizontal", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.OPPOSITE, + name="Reverse", + icon="mdi:swap-horizontal", + entity_category=EntityCategory.CONFIG, + ), + ), + # Humidifier + # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b + "jsq": ( + SwitchEntityDescription( + key=DPCode.SWITCH_SOUND, + name="Voice", + icon="mdi:account-voice", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.SLEEP, + name="Sleep", + icon="mdi:power-sleep", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.STERILIZATION, + name="Sterilization", + icon="mdi:minus-circle-outline", + entity_category=EntityCategory.CONFIG, + ), + ), } # Socket (duplicate of `pc`) diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json index 7f23e4091658c..9a406ffcb4be7 100644 --- a/homeassistant/components/tuya/translations/cs.json +++ b/homeassistant/components/tuya/translations/cs.json @@ -10,6 +10,11 @@ }, "flow_title": "Konfigurace Tuya", "step": { + "login": { + "data": { + "country_code": "K\u00f3d zem\u011b" + } + }, "user": { "data": { "country_code": "Zem\u011b", diff --git a/homeassistant/components/tuya/translations/el.json b/homeassistant/components/tuya/translations/el.json index 2fb4b8cf7204f..63ba3b2696a13 100644 --- a/homeassistant/components/tuya/translations/el.json +++ b/homeassistant/components/tuya/translations/el.json @@ -1,11 +1,38 @@ { "config": { + "abort": { + "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", + "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": { + "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", + "login_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 ({code}): {msg}" + }, "flow_title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Tuya", "step": { + "login": { + "data": { + "access_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "access_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "country_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03ce\u03c1\u03b1\u03c2", + "endpoint": "\u0396\u03ce\u03bd\u03b7 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "tuya_app_type": "Mobile App", + "username": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2" + }, + "description": "\u0395\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 Tuya", + "title": "Tuya" + }, "user": { "data": { + "access_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Tuya IoT", + "access_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Tuya IoT", "country_code": "\u03a7\u03ce\u03c1\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "platform": "\u0397 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2", + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae", + "tuya_project_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 Tuya cloud", "username": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 Tuya", @@ -14,13 +41,42 @@ } }, "options": { + "abort": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "dev_multi_type": "\u03a0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03c4\u03cd\u03c0\u03bf", + "dev_not_config": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "dev_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" + }, "step": { "device": { "data": { + "brightness_range_mode": "\u0395\u03cd\u03c1\u03bf\u03c2 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "curr_temp_divider": "\u0394\u03b9\u03b1\u03b9\u03c1\u03ad\u03c4\u03b7\u03c2 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 (0 = \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2)", + "max_kelvin": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf kelvin", + "max_temp": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c2 (\u03c7\u03c1\u03ae\u03c3\u03b7 min \u03ba\u03b1\u03b9 max = 0 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", + "min_kelvin": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 kelvin", + "min_temp": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c2 (\u03c7\u03c1\u03ae\u03c3\u03b7 min \u03ba\u03b1\u03b9 max = 0 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", "set_temp_divided": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b9\u03b1\u03b9\u03c1\u03b5\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2", - "temp_step_override": "\u0392\u03ae\u03bc\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf\u03c5" + "support_color": "\u0391\u03bd\u03b1\u03b3\u03ba\u03b1\u03c3\u03c4\u03b9\u03ba\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2", + "temp_divider": "\u0394\u03b9\u03b1\u03b9\u03c1\u03ad\u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ce\u03bd \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 (0 = \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2)", + "temp_step_override": "\u0392\u03ae\u03bc\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf\u03c5", + "tuya_max_coltemp": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c4\u03c9\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {device_type} `{device_name}`", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Tuya" + }, + "init": { + "data": { + "discovery_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1", + "list_devices": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03ae \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03ba\u03b5\u03bd\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", + "query_device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b5\u03c1\u03c9\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "query_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1" + }, + "description": "\u039c\u03b7\u03bd \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b5 \u03c0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03ad\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b4\u03b9\u03b1\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03bf\u03c0\u03ae\u03c3\u03b5\u03c9\u03bd, \u03b1\u03bb\u03bb\u03b9\u03ce\u03c2 \u03bf\u03b9 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c4\u03cd\u03c7\u03bf\u03c5\u03bd \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Tuya" } } } diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json index d9159ce954da4..242d1e9ee0896 100644 --- a/homeassistant/components/tuya/translations/pt-BR.json +++ b/homeassistant/components/tuya/translations/pt-BR.json @@ -1,29 +1,49 @@ { "config": { "abort": { - "cannot_connect": "Falhou ao conectar", - "invalid_auth": "{%component::tuya::config::error::invalid_auth%}", - "single_instance_allowed": "J\u00e1 configurado. S\u00f3 \u00e9 poss\u00edvel uma \u00fanica configura\u00e7\u00e3o." + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "login_error": "Erro de login ({code}): {msg}" }, "flow_title": "Configura\u00e7\u00e3o Tuya", "step": { + "login": { + "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", + "country_code": "Pa\u00eds", + "endpoint": "Regi\u00e3o", + "password": "Senha do Aplicativo", + "tuya_app_type": "O aplicativo onde sua conta \u00e9 registrada", + "username": "Usu\u00e1rio do Aplicativo" + }, + "description": "Digite sua credencial Tuya", + "title": "Integra\u00e7\u00e3o Tuya" + }, "user": { "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "Pa\u00eds", - "password": "Senha", + "password": "Senha do Aplicativo", "platform": "O aplicativo onde sua conta \u00e9 registrada", "region": "Regi\u00e3o", - "username": "Nome de usu\u00e1rio" + "tuya_project_type": "Tipo de projeto de Tuya Cloud", + "username": "Usu\u00e1rio do Aplicativo" }, - "description": "Digite sua credencial Tuya.", + "description": "Digite sua credencial Tuya", "title": "Integra\u00e7\u00e3o Tuya" } } }, "options": { + "abort": { + "cannot_connect": "Falha ao conectar" + }, "error": { "dev_multi_type": "V\u00e1rios dispositivos selecionados para configurar devem ser do mesmo tipo", "dev_not_config": "Tipo de dispositivo n\u00e3o configur\u00e1vel", @@ -38,8 +58,10 @@ "max_temp": "Temperatura m\u00e1xima do alvo (use min e max = 0 para padr\u00e3o)", "min_kelvin": "Temperatura m\u00ednima de cor suportada em kelvin", "min_temp": "Temperatura m\u00ednima desejada (use m\u00edn e m\u00e1x = 0 para o padr\u00e3o)", + "set_temp_divided": "Use o valor de temperatura dividido para o comando de temperatura definido", "support_color": "For\u00e7ar suporte de cores", "temp_divider": "Divisor de valores de temperatura (0 = usar padr\u00e3o)", + "temp_step_override": "Etapa de temperatura alvo", "tuya_max_coltemp": "Temperatura m\u00e1xima de cor relatada pelo dispositivo", "unit_of_measurement": "Unidade de temperatura usada pelo dispositivo" }, @@ -49,8 +71,12 @@ "init": { "data": { "discovery_interval": "Intervalo de pesquisa do dispositivo de descoberta em segundos", - "list_devices": "Selecione os dispositivos para configurar ou deixe em branco para salvar a configura\u00e7\u00e3o" - } + "list_devices": "Selecione os dispositivos para configurar ou deixe em branco para salvar a configura\u00e7\u00e3o", + "query_device": "Selecione o dispositivo que usar\u00e1 o m\u00e9todo de consulta para atualiza\u00e7\u00e3o de status mais r\u00e1pida", + "query_interval": "Intervalo de sondagem do dispositivo de consulta em segundos" + }, + "description": "N\u00e3o defina valores de intervalo de sondagens muito baixos ou as chamadas falhar\u00e3o gerando mensagem de erro no log", + "title": "Configurar op\u00e7\u00f5es do Tuya" } } } diff --git a/homeassistant/components/tuya/translations/select.bg.json b/homeassistant/components/tuya/translations/select.bg.json index 4f70234402993..4e46bd55033d4 100644 --- a/homeassistant/components/tuya/translations/select.bg.json +++ b/homeassistant/components/tuya/translations/select.bg.json @@ -9,10 +9,47 @@ "1": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "2": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, + "tuya__countdown": { + "1h": "1 \u0447\u0430\u0441", + "2h": "2 \u0447\u0430\u0441\u0430", + "3h": "3 \u0447\u0430\u0441\u0430", + "4h": "4 \u0447\u0430\u0441\u0430", + "5h": "5 \u0447\u0430\u0441\u0430", + "6h": "6 \u0447\u0430\u0441\u0430", + "cancel": "\u041e\u0442\u043a\u0430\u0437" + }, + "tuya__curtain_mode": { + "morning": "\u0421\u0443\u0442\u0440\u0438\u043d", + "night": "\u041d\u043e\u0449" + }, + "tuya__curtain_motor_mode": { + "back": "\u041d\u0430\u0437\u0430\u0434", + "forward": "\u041d\u0430\u043f\u0440\u0435\u0434" + }, "tuya__decibel_sensitivity": { "0": "\u041d\u0438\u0441\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442", "1": "\u0412\u0438\u0441\u043e\u043a\u0430 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__humidifier_level": { + "level_1": "\u041d\u0438\u0432\u043e 1", + "level_10": "\u041d\u0438\u0432\u043e 10", + "level_2": "\u041d\u0438\u0432\u043e 2", + "level_3": "\u041d\u0438\u0432\u043e 3", + "level_4": "\u041d\u0438\u0432\u043e 4", + "level_5": "\u041d\u0438\u0432\u043e 5", + "level_6": "\u041d\u0438\u0432\u043e 6", + "level_7": "\u041d\u0438\u0432\u043e 7", + "level_8": "\u041d\u0438\u0432\u043e 8", + "level_9": "\u041d\u0438\u0432\u043e 9" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e" + }, "tuya__led_type": { "halogen": "\u0425\u0430\u043b\u043e\u0433\u0435\u043d\u043d\u0438", "incandescent": "\u0421 \u043d\u0430\u0436\u0435\u0436\u0430\u0435\u043c\u0430 \u0436\u0438\u0447\u043a\u0430", @@ -35,6 +72,8 @@ "power_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "tuya__vacuum_mode": { + "point": "\u0422\u043e\u0447\u043a\u0430", + "pose": "\u041f\u043e\u0437\u0430", "zone": "\u0417\u043e\u043d\u0430" } } diff --git a/homeassistant/components/tuya/translations/select.ca.json b/homeassistant/components/tuya/translations/select.ca.json index d3de5c908684a..2727efd1a08f8 100644 --- a/homeassistant/components/tuya/translations/select.ca.json +++ b/homeassistant/components/tuya/translations/select.ca.json @@ -10,14 +10,62 @@ "1": "OFF", "2": "ON" }, + "tuya__countdown": { + "1h": "1 hora", + "2h": "2 hores", + "3h": "3 hores", + "4h": "4 hores", + "5h": "5 hores", + "6h": "6 hores", + "cancel": "Cancel\u00b7la" + }, + "tuya__curtain_mode": { + "morning": "Mat\u00ed", + "night": "Nit" + }, + "tuya__curtain_motor_mode": { + "back": "Enrere", + "forward": "Endavant" + }, "tuya__decibel_sensitivity": { "0": "Sensibilitat baixa", "1": "Sensibilitat alta" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Polsador", "switch": "Interruptor" }, + "tuya__humidifier_level": { + "level_1": "Nivell 1", + "level_10": "Nivell 10", + "level_2": "Nivell 2", + "level_3": "Nivell 3", + "level_4": "Nivell 4", + "level_5": "Nivell 5", + "level_6": "Nivell 6", + "level_7": "Nivell 7", + "level_8": "Nivell 8", + "level_9": "Nivell 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Estat 1", + "2": "Estat 2", + "3": "Estat 3", + "4": "Estat 4", + "5": "Estat 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Autom\u00e0tic", + "health": "Salut", + "humidity": "Humitat", + "sleep": "Dormir", + "work": "Feina" + }, "tuya__ipc_work_mode": { "0": "Mode de baix consum", "1": "Mode de funcionament continu" diff --git a/homeassistant/components/tuya/translations/select.de.json b/homeassistant/components/tuya/translations/select.de.json index 1577872391e58..d63c92365cbc3 100644 --- a/homeassistant/components/tuya/translations/select.de.json +++ b/homeassistant/components/tuya/translations/select.de.json @@ -10,14 +10,62 @@ "1": "Aus", "2": "An" }, + "tuya__countdown": { + "1h": "1 Stunde", + "2h": "2 Stunden", + "3h": "3 Stunden", + "4h": "4 Stunden", + "5h": "5 Stunden", + "6h": "6 Stunden", + "cancel": "Abbrechen" + }, + "tuya__curtain_mode": { + "morning": "Morgen", + "night": "Nacht" + }, + "tuya__curtain_motor_mode": { + "back": "Zur\u00fcck", + "forward": "Vorw\u00e4rts" + }, "tuya__decibel_sensitivity": { "0": "Geringe Empfindlichkeit", "1": "Hohe Empfindlichkeit" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Dr\u00fccken", "switch": "Schalter" }, + "tuya__humidifier_level": { + "level_1": "Stufe 1", + "level_10": "Stufe 10", + "level_2": "Stufe 2", + "level_3": "Stufe 3", + "level_4": "Stufe 4", + "level_5": "Stufe 5", + "level_6": "Stufe 6", + "level_7": "Stufe 7", + "level_8": "Stufe 8", + "level_9": "Stufe 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Stimmung 1", + "2": "Stimmung 2", + "3": "Stimmung 3", + "4": "Stimmung 4", + "5": "Stimmung 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Automatisch", + "health": "Gesundheit", + "humidity": "Luftfeuchtigkeit", + "sleep": "Schlafen", + "work": "Arbeit" + }, "tuya__ipc_work_mode": { "0": "Energiesparmodus", "1": "Kontinuierlicher Arbeitsmodus" diff --git a/homeassistant/components/tuya/translations/select.el.json b/homeassistant/components/tuya/translations/select.el.json index 90b677d331f5a..45a09f368f7c9 100644 --- a/homeassistant/components/tuya/translations/select.el.json +++ b/homeassistant/components/tuya/translations/select.el.json @@ -6,12 +6,97 @@ "2": "60 Hz" }, "tuya__basic_nightvision": { - "0": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf" + "0": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "1": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "2": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + }, + "tuya__countdown": { + "1h": "1 \u03ce\u03c1\u03b1", + "2h": "2 \u03ce\u03c1\u03b5\u03c2", + "3h": "3 \u03ce\u03c1\u03b5\u03c2", + "4h": "4 \u03ce\u03c1\u03b5\u03c2", + "5h": "5 \u03ce\u03c1\u03b5\u03c2", + "6h": "6 \u03ce\u03c1\u03b5\u03c2", + "cancel": "\u0391\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7" + }, + "tuya__curtain_mode": { + "morning": "\u03a0\u03c1\u03c9\u03af", + "night": "\u039d\u03cd\u03c7\u03c4\u03b1" + }, + "tuya__curtain_motor_mode": { + "back": "\u03a0\u03af\u03c3\u03c9", + "forward": "\u0395\u03bc\u03c0\u03c1\u03cc\u03c2" + }, + "tuya__decibel_sensitivity": { + "0": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae \u03b5\u03c5\u03b1\u03b9\u03c3\u03b8\u03b7\u03c3\u03af\u03b1", + "1": "\u03a5\u03c8\u03b7\u03bb\u03ae \u03b5\u03c5\u03b1\u03b9\u03c3\u03b8\u03b7\u03c3\u03af\u03b1" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" }, "tuya__fingerbot_mode": { "click": "\u03a0\u03af\u03b5\u03c3\u03b5", "switch": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2" }, + "tuya__humidifier_level": { + "level_1": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 1", + "level_10": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 10", + "level_2": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 2", + "level_3": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 3", + "level_4": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 4", + "level_5": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 5", + "level_6": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 6", + "level_7": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 7", + "level_8": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 8", + "level_9": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf 9" + }, + "tuya__humidifier_moodlighting": { + "1": "\u0394\u03b9\u03ac\u03b8\u03b5\u03c3\u03b7 1", + "2": "\u0394\u03b9\u03ac\u03b8\u03b5\u03c3\u03b7 2", + "3": "\u0394\u03b9\u03ac\u03b8\u03b5\u03c3\u03b7 3", + "4": "\u0394\u03b9\u03ac\u03b8\u03b5\u03c3\u03b7 4", + "5": "\u0394\u03b9\u03ac\u03b8\u03b5\u03c3\u03b7 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "health": "\u03a5\u03b3\u03b5\u03af\u03b1", + "humidity": "\u03a5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1", + "sleep": "\u038e\u03c0\u03bd\u03bf\u03c2", + "work": "\u0395\u03c1\u03b3\u03b1\u03c3\u03af\u03b1" + }, + "tuya__ipc_work_mode": { + "0": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03b1\u03bc\u03b7\u03bb\u03ae\u03c2 \u03b9\u03c3\u03c7\u03cd\u03bf\u03c2", + "1": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03bf\u03cd\u03c2 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2" + }, + "tuya__led_type": { + "halogen": "\u0391\u03bb\u03bf\u03b3\u03cc\u03bd\u03bf\u03c5", + "incandescent": "\u03a0\u03c5\u03c1\u03b1\u03ba\u03c4\u03ce\u03c3\u03b5\u03c9\u03c2", + "led": "LED" + }, + "tuya__light_mode": { + "none": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "pos": "\u03a5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b8\u03ad\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7", + "relay": "\u0388\u03bd\u03b4\u03b5\u03b9\u03be\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2/\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" + }, + "tuya__motion_sensitivity": { + "0": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae \u03b5\u03c5\u03b1\u03b9\u03c3\u03b8\u03b7\u03c3\u03af\u03b1", + "1": "\u039c\u03b5\u03c3\u03b1\u03af\u03b1 \u03b5\u03c5\u03b1\u03b9\u03c3\u03b8\u03b7\u03c3\u03af\u03b1", + "2": "\u03a5\u03c8\u03b7\u03bb\u03ae \u03b5\u03c5\u03b1\u03b9\u03c3\u03b8\u03b7\u03c3\u03af\u03b1" + }, + "tuya__record_mode": { + "1": "\u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03bc\u03cc\u03bd\u03bf", + "2": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03ae\u03c2 \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae" + }, + "tuya__relay_status": { + "last": "\u0398\u03c5\u03bc\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "memory": "\u0398\u03c5\u03bc\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "power_off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "power_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + }, "tuya__vacuum_cistern": { "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", @@ -19,15 +104,29 @@ "middle": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf" }, "tuya__vacuum_collection": { - "large": "\u039c\u03b5\u03b3\u03ac\u03bb\u03bf" + "large": "\u039c\u03b5\u03b3\u03ac\u03bb\u03bf", + "middle": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", + "small": "\u039c\u03b9\u03ba\u03c1\u03cc" }, "tuya__vacuum_mode": { + "bow": "\u03a4\u03cc\u03be\u03bf", "chargego": "\u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03c3\u03c4\u03b7 \u03b2\u03ac\u03c3\u03b7", + "left_bow": "\u03a4\u03cc\u03be\u03bf \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "left_spiral": "\u03a3\u03c0\u03b9\u03c1\u03ac\u03bb \u0391\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", "mop": "\u03a3\u03c6\u03bf\u03c5\u03b3\u03b3\u03ac\u03c1\u03b9\u03c3\u03bc\u03b1", + "part": "\u039c\u03ad\u03c1\u03bf\u03c2", + "partial_bow": "\u03a4\u03cc\u03be\u03bf \u03b5\u03bd \u03bc\u03ad\u03c1\u03b5\u03b9", "pick_zone": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u0396\u03ce\u03bd\u03b7\u03c2", + "point": "\u03a3\u03b7\u03bc\u03b5\u03af\u03bf", + "pose": "\u03a3\u03c4\u03ac\u03c3\u03b7", "random": "\u03a4\u03c5\u03c7\u03b1\u03af\u03bf", + "right_bow": "\u03a4\u03cc\u03be\u03bf \u0394\u03b5\u03be\u03b9\u03ac", + "right_spiral": "\u03a3\u03c0\u03b9\u03c1\u03ac\u03bb \u0394\u03b5\u03be\u03b9\u03ac", + "single": "\u039c\u03bf\u03bd\u03cc", "smart": "\u0388\u03be\u03c5\u03c0\u03bd\u03bf", + "spiral": "\u03a3\u03c0\u03b9\u03c1\u03ac\u03bb", "standby": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2", + "wall_follow": "\u0391\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b5 \u03c4\u03bf\u03bd \u03c4\u03bf\u03af\u03c7\u03bf", "zone": "\u0396\u03ce\u03bd\u03b7" } } diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index 08756130a79ef..65d4bdeca80ac 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -10,14 +10,62 @@ "1": "Off", "2": "On" }, + "tuya__countdown": { + "1h": "1 hour", + "2h": "2 hours", + "3h": "3 hours", + "4h": "4 hours", + "5h": "5 hours", + "6h": "6 hours", + "cancel": "Cancel" + }, + "tuya__curtain_mode": { + "morning": "Morning", + "night": "Night" + }, + "tuya__curtain_motor_mode": { + "back": "Back", + "forward": "Forward" + }, "tuya__decibel_sensitivity": { "0": "Low sensitivity", "1": "High sensitivity" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Push", "switch": "Switch" }, + "tuya__humidifier_level": { + "level_1": "Level 1", + "level_10": "Level 10", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Mood 1", + "2": "Mood 2", + "3": "Mood 3", + "4": "Mood 4", + "5": "Mood 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Health", + "humidity": "Humidity", + "sleep": "Sleep", + "work": "Work" + }, "tuya__ipc_work_mode": { "0": "Low power mode", "1": "Continuous working mode" diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index d0552cb6d33c8..adc306feae47b 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -10,6 +10,15 @@ "1": "Apagado", "2": "Encendido" }, + "tuya__countdown": { + "1h": "1 hora", + "2h": "2 horas", + "3h": "3 horas", + "4h": "4 horas", + "5h": "5 horas", + "6h": "6 horas", + "cancel": "Cancelar" + }, "tuya__decibel_sensitivity": { "0": "Sensibilidad baja", "1": "Sensibilidad alta" @@ -18,6 +27,16 @@ "click": "Push", "switch": "Interruptor" }, + "tuya__humidifier_level": { + "level_1": "Nivel 1", + "level_10": "Nivel 10" + }, + "tuya__humidifier_spray_mode": { + "health": "Salud", + "humidity": "Humedad", + "sleep": "Dormir", + "work": "Trabajo" + }, "tuya__ipc_work_mode": { "0": "Modo de bajo consumo", "1": "Modo de trabajo continuo" diff --git a/homeassistant/components/tuya/translations/select.et.json b/homeassistant/components/tuya/translations/select.et.json index a2559065018fa..eba76886439a2 100644 --- a/homeassistant/components/tuya/translations/select.et.json +++ b/homeassistant/components/tuya/translations/select.et.json @@ -10,14 +10,62 @@ "1": "V\u00e4ljas", "2": "Sees" }, + "tuya__countdown": { + "1h": "1 tund", + "2h": "2 tundi", + "3h": "3 tundi", + "4h": "4 tundi", + "5h": "5 tundi", + "6h": "6 tundi", + "cancel": "Loobu" + }, + "tuya__curtain_mode": { + "morning": "Hommik", + "night": "\u00d6\u00f6" + }, + "tuya__curtain_motor_mode": { + "back": "Tagasi", + "forward": "Edasi" + }, "tuya__decibel_sensitivity": { "0": "Madal tundlikkus", "1": "K\u00f5rge tundlikkus" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Vajutus", "switch": "L\u00fcliti" }, + "tuya__humidifier_level": { + "level_1": "Tase 1", + "level_10": "Tase 10", + "level_2": "Tase 2", + "level_3": "Tase 3", + "level_4": "Tase 4", + "level_5": "Tase 5", + "level_6": "Tase 6", + "level_7": "Tase 7", + "level_8": "Tase 8", + "level_9": "Tase 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Meeleolu 1", + "2": "Meeleolu 2", + "3": "Meeleolu 3", + "4": "Meeleolu 4", + "5": "Meeleolu 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Automaatne", + "health": "Tervis", + "humidity": "Niiskus", + "sleep": "Uneaeg", + "work": "T\u00f6\u00f6aeg" + }, "tuya__ipc_work_mode": { "0": "Madala energiatarbega re\u017eiim", "1": "Pidev t\u00f6\u00f6re\u017eiim" diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json index a01a11bf5df83..ab67be514bcdd 100644 --- a/homeassistant/components/tuya/translations/select.fr.json +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -10,14 +10,62 @@ "1": "Inactif", "2": "Actif" }, + "tuya__countdown": { + "1h": "1 heure", + "2h": "2 heures", + "3h": "3 heures", + "4h": "4 heures", + "5h": "5 heures", + "6h": "6 heures", + "cancel": "Annuler" + }, + "tuya__curtain_mode": { + "morning": "Matin", + "night": "Nuit" + }, + "tuya__curtain_motor_mode": { + "back": "Retour", + "forward": "Avance rapide" + }, "tuya__decibel_sensitivity": { "0": "Faible sensibilit\u00e9", "1": "Haute sensibilit\u00e9" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Appuyer", "switch": "Interrupteur" }, + "tuya__humidifier_level": { + "level_1": "Niveau 1", + "level_10": "Niveau 10", + "level_2": "Niveau 2", + "level_3": "Niveau 3", + "level_4": "Niveau 4", + "level_5": "Niveau 5", + "level_6": "Niveau 6", + "level_7": "Niveau 7", + "level_8": "Niveau 8", + "level_9": "Niveau 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Humeur 1", + "2": "Humeur 2", + "3": "Humeur 3", + "4": "Humeur 4", + "5": "Humeur 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Sant\u00e9", + "humidity": "Humidit\u00e9", + "sleep": "Sommeil", + "work": "Travail" + }, "tuya__ipc_work_mode": { "0": "Mode faible consommation", "1": "Mode de travail continu" @@ -51,7 +99,9 @@ }, "tuya__vacuum_cistern": { "closed": "Ferm\u00e9", - "high": "Haut" + "high": "Haut", + "low": "Faible", + "middle": "Milieu" }, "tuya__vacuum_collection": { "large": "Grand", @@ -59,11 +109,16 @@ "small": "Petit" }, "tuya__vacuum_mode": { + "bow": "Arc", "chargego": "Retour \u00e0 la base", + "left_bow": "Arc gauche", "left_spiral": "Spirale gauche", "mop": "Serpilli\u00e8re", + "part": "Partie", "partial_bow": "Arc partiel", "pick_zone": "S\u00e9lectionner une zone", + "point": "Point", + "pose": "Pose", "random": "Al\u00e9atoire", "right_bow": "Arc \u00e0 droite", "right_spiral": "Spirale droite", diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index f3555baadfe84..eacaecacb07ae 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -8,10 +8,58 @@ "1": "\u05db\u05d1\u05d5\u05d9", "2": "\u05de\u05d5\u05e4\u05e2\u05dc" }, + "tuya__countdown": { + "1h": "\u05e9\u05e2\u05d4", + "2h": "\u05e9\u05e2\u05ea\u05d9\u05d9\u05dd", + "3h": "3 \u05e9\u05e2\u05d5\u05ea", + "4h": "4 \u05e9\u05e2\u05d5\u05ea", + "5h": "5 \u05e9\u05e2\u05d5\u05ea", + "6h": "6 \u05e9\u05e2\u05d5\u05ea", + "cancel": "\u05d1\u05d9\u05d8\u05d5\u05dc" + }, + "tuya__curtain_mode": { + "morning": "\u05d1\u05d5\u05e7\u05e8", + "night": "\u05dc\u05d9\u05dc\u05d4" + }, + "tuya__curtain_motor_mode": { + "back": "\u05d7\u05d6\u05d5\u05e8", + "forward": "\u05e7\u05d3\u05d9\u05de\u05d4" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "\u05d3\u05d7\u05d9\u05e4\u05d4", "switch": "\u05de\u05ea\u05d2" }, + "tuya__humidifier_level": { + "level_1": "\u05e8\u05de\u05d4 1", + "level_10": "\u05e8\u05de\u05d4 10", + "level_2": "\u05e8\u05de\u05d4 2", + "level_3": "\u05e8\u05de\u05d4 3", + "level_4": "\u05e8\u05de\u05d4 4", + "level_5": "\u05e8\u05de\u05d4 5", + "level_6": "\u05e8\u05de\u05d4 6", + "level_7": "\u05e8\u05de\u05d4 7", + "level_8": "\u05e8\u05de\u05d4 8", + "level_9": "\u05e8\u05de\u05d4 9" + }, + "tuya__humidifier_moodlighting": { + "1": "\u05de\u05e6\u05d1 \u05e8\u05d5\u05d7 1", + "2": "\u05de\u05e6\u05d1 \u05e8\u05d5\u05d7 2", + "3": "\u05de\u05e6\u05d1 \u05e8\u05d5\u05d7 3", + "4": "\u05de\u05e6\u05d1 \u05e8\u05d5\u05d7 4", + "5": "\u05de\u05e6\u05d1 \u05e8\u05d5\u05d7 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", + "health": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea", + "humidity": "\u05dc\u05d7\u05d5\u05ea", + "sleep": "\u05e9\u05d9\u05e0\u05d4", + "work": "\u05e2\u05d1\u05d5\u05d3\u05d4" + }, "tuya__led_type": { "led": "\u05dc\u05d3" }, diff --git a/homeassistant/components/tuya/translations/select.hu.json b/homeassistant/components/tuya/translations/select.hu.json index f1df3a531e85e..23a1d29adebde 100644 --- a/homeassistant/components/tuya/translations/select.hu.json +++ b/homeassistant/components/tuya/translations/select.hu.json @@ -10,14 +10,62 @@ "1": "Ki", "2": "Be" }, + "tuya__countdown": { + "1h": "1 \u00f3ra", + "2h": "2 \u00f3ra", + "3h": "3 \u00f3ra", + "4h": "4 \u00f3ra", + "5h": "5 \u00f3ra", + "6h": "6 \u00f3ra", + "cancel": "M\u00e9gse" + }, + "tuya__curtain_mode": { + "morning": "Reggel", + "night": "\u00c9jszaka" + }, + "tuya__curtain_motor_mode": { + "back": "Vissza", + "forward": "El\u0151re" + }, "tuya__decibel_sensitivity": { "0": "Alacsony \u00e9rz\u00e9kenys\u00e9g", "1": "Magas \u00e9rz\u00e9kenys\u00e9g" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Lenyom\u00e1s", "switch": "Kapcsol\u00e1s" }, + "tuya__humidifier_level": { + "level_1": "1. szint", + "level_10": "10. szint", + "level_2": "2. szint", + "level_3": "3. szint", + "level_4": "4. szint", + "level_5": "5. szint", + "level_6": "6. szint", + "level_7": "7. szint", + "level_8": "8. szint", + "level_9": "9. szint" + }, + "tuya__humidifier_moodlighting": { + "1": "1. hangulat", + "2": "2. hangulat", + "3": "3. hangulat", + "4": "4. hangulat", + "5": "5. hangulat" + }, + "tuya__humidifier_spray_mode": { + "auto": "Automatikus", + "health": "Eg\u00e9szs\u00e9g", + "humidity": "P\u00e1ratartalom", + "sleep": "Alv\u00e1s", + "work": "Munka" + }, "tuya__ipc_work_mode": { "0": "Alacsony fogyaszt\u00e1s\u00fa m\u00f3d", "1": "Folyamatos \u00fczemm\u00f3d" diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json index 4ea665e4a79c1..9b404daf61289 100644 --- a/homeassistant/components/tuya/translations/select.id.json +++ b/homeassistant/components/tuya/translations/select.id.json @@ -10,14 +10,62 @@ "1": "Mati", "2": "Nyala" }, + "tuya__countdown": { + "1h": "1 jam", + "2h": "2 jam", + "3h": "3 jam", + "4h": "4 jam", + "5h": "5 jam", + "6h": "6 jam", + "cancel": "Batalkan" + }, + "tuya__curtain_mode": { + "morning": "Pagi", + "night": "Malam" + }, + "tuya__curtain_motor_mode": { + "back": "Mundur", + "forward": "Maju" + }, "tuya__decibel_sensitivity": { "0": "Sensitivitas rendah", "1": "Sensitivitas tinggi" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Dorong", "switch": "Sakelar" }, + "tuya__humidifier_level": { + "level_1": "Tingkat 1", + "level_10": "Tingkat 10", + "level_2": "Tingkat 2", + "level_3": "Tingkat 3", + "level_4": "Tingkat 4", + "level_5": "Tingkat 5", + "level_6": "Tingkat 6", + "level_7": "Tingkat 7", + "level_8": "Tingkat 8", + "level_9": "Tingkat 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Suasana 1", + "2": "Suasana 2", + "3": "Suasana 3", + "4": "Suasana 4", + "5": "Suasana 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Otomatis", + "health": "Kesehatan", + "humidity": "Kelembaban", + "sleep": "Tidur", + "work": "Bekerja" + }, "tuya__ipc_work_mode": { "0": "Mode daya rendah", "1": "Mode kerja terus menerus" @@ -48,6 +96,33 @@ "on": "Nyala", "power_off": "Mati", "power_on": "Nyala" + }, + "tuya__vacuum_cistern": { + "closed": "Tutup", + "high": "Tinggi", + "low": "Rendah", + "middle": "Tengah" + }, + "tuya__vacuum_collection": { + "large": "Besar", + "middle": "Tengah", + "small": "Kecil" + }, + "tuya__vacuum_mode": { + "chargego": "Kembali ke dock", + "left_spiral": "Spiral Kiri", + "mop": "Pel", + "part": "Bagian", + "pick_zone": "Pilih Zona", + "point": "Titik", + "random": "Acak", + "right_spiral": "Spiral Kanan", + "single": "Tunggal", + "smart": "Cerdas", + "spiral": "Spiral", + "standby": "Siaga", + "wall_follow": "Ikuti Dinding", + "zone": "Zona" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.it.json b/homeassistant/components/tuya/translations/select.it.json index 80b86ec495c7f..dde91a52d9a7c 100644 --- a/homeassistant/components/tuya/translations/select.it.json +++ b/homeassistant/components/tuya/translations/select.it.json @@ -10,14 +10,62 @@ "1": "Spento", "2": "Acceso" }, + "tuya__countdown": { + "1h": "1 ora", + "2h": "2 ore", + "3h": "3 ore", + "4h": "4 ore", + "5h": "5 ore", + "6h": "6 ore", + "cancel": "Annulla" + }, + "tuya__curtain_mode": { + "morning": "Mattina", + "night": "Notte" + }, + "tuya__curtain_motor_mode": { + "back": "Indietro", + "forward": "Avanti" + }, "tuya__decibel_sensitivity": { "0": "Bassa sensibilit\u00e0", "1": "Alta sensibilit\u00e0" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Spingere", "switch": "Interruttore" }, + "tuya__humidifier_level": { + "level_1": "Livello 1", + "level_10": "Livello 10", + "level_2": "Livello 2", + "level_3": "Livello 3", + "level_4": "Livello 4", + "level_5": "Livello 5", + "level_6": "Livello 6", + "level_7": "Livello 7", + "level_8": "Livello 8", + "level_9": "Livello 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Umore 1", + "2": "Umore 2", + "3": "Umore 3", + "4": "Umore 4", + "5": "Umore 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Automatico", + "health": "Salute", + "humidity": "Umidit\u00e0", + "sleep": "Sonno", + "work": "Lavoro" + }, "tuya__ipc_work_mode": { "0": "Modalit\u00e0 a basso consumo", "1": "Modalit\u00e0 di lavoro continua" diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index 57d4297fd6833..5712544feb312 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -10,14 +10,62 @@ "1": "\u30aa\u30d5", "2": "\u30aa\u30f3" }, + "tuya__countdown": { + "1h": "1\u6642\u9593", + "2h": "2\u6642\u9593", + "3h": "3\u6642\u9593", + "4h": "4\u6642\u9593", + "5h": "5\u6642\u9593", + "6h": "6\u6642\u9593", + "cancel": "\u30ad\u30e3\u30f3\u30bb\u30eb" + }, + "tuya__curtain_mode": { + "morning": "\u671d", + "night": "\u591c" + }, + "tuya__curtain_motor_mode": { + "back": "\u623b\u308b", + "forward": "\u9032\u3080" + }, "tuya__decibel_sensitivity": { "0": "\u4f4e\u611f\u5ea6", "1": "\u9ad8\u611f\u5ea6" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "\u62bc\u3059", "switch": "\u30b9\u30a4\u30c3\u30c1" }, + "tuya__humidifier_level": { + "level_1": "\u30ec\u30d9\u30eb 1", + "level_10": "\u30ec\u30d9\u30eb 10", + "level_2": "\u30ec\u30d9\u30eb 2", + "level_3": "\u30ec\u30d9\u30eb 3", + "level_4": "\u30ec\u30d9\u30eb 4", + "level_5": "\u30ec\u30d9\u30eb 5", + "level_6": "\u30ec\u30d9\u30eb 6", + "level_7": "\u30ec\u30d9\u30eb 7", + "level_8": "\u30ec\u30d9\u30eb 8", + "level_9": "\u30ec\u30d9\u30eb 9" + }, + "tuya__humidifier_moodlighting": { + "1": "\u30e0\u30fc\u30c9 1", + "2": "\u30e0\u30fc\u30c9 2", + "3": "\u30e0\u30fc\u30c9 3", + "4": "\u30e0\u30fc\u30c9 4", + "5": "\u30e0\u30fc\u30c9 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u30aa\u30fc\u30c8", + "health": "\u30d8\u30eb\u30b9", + "humidity": "\u6e7f\u5ea6", + "sleep": "\u30b9\u30ea\u30fc\u30d7", + "work": "\u30ef\u30fc\u30af" + }, "tuya__ipc_work_mode": { "0": "\u4f4e\u96fb\u529b\u30e2\u30fc\u30c9", "1": "\u9023\u7d9a\u4f5c\u696d\u30e2\u30fc\u30c9" diff --git a/homeassistant/components/tuya/translations/select.lv.json b/homeassistant/components/tuya/translations/select.lv.json new file mode 100644 index 0000000000000..4c86485bcc289 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.lv.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__countdown": { + "1h": "1 stunda", + "2h": "2 stundas", + "3h": "3 stundas", + "4h": "4 stundas", + "5h": "5 stundas", + "6h": "6 stundas", + "cancel": "Atcelt" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__vacuum_mode": { + "wall_follow": "Sekot sienai", + "zone": "Zona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.nb.json b/homeassistant/components/tuya/translations/select.nb.json new file mode 100644 index 0000000000000..f7653b352e49f --- /dev/null +++ b/homeassistant/components/tuya/translations/select.nb.json @@ -0,0 +1,9 @@ +{ + "state": { + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index 0961f6a29fd1c..e645694ae55de 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -10,14 +10,62 @@ "1": "Uit", "2": "Aan" }, + "tuya__countdown": { + "1h": "1 uur", + "2h": "2 uur", + "3h": "3 uur", + "4h": "4 uur", + "5h": "5 uur", + "6h": "6 uur", + "cancel": "Annuleren" + }, + "tuya__curtain_mode": { + "morning": "Ochtend", + "night": "Nacht" + }, + "tuya__curtain_motor_mode": { + "back": "Terug", + "forward": "Vooruit" + }, "tuya__decibel_sensitivity": { "0": "Lage gevoeligheid", "1": "Hoge gevoeligheid" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Duw", "switch": "Schakelaar" }, + "tuya__humidifier_level": { + "level_1": "Niveau 1", + "level_10": "Niveau 10", + "level_2": "Niveau 2", + "level_3": "Niveau 3", + "level_4": "Niveau 4", + "level_5": "Niveau 5", + "level_6": "Niveau 6", + "level_7": "Niveau 7", + "level_8": "Niveau 8", + "level_9": "Niveau 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Stemming 1", + "2": "Stemming 2", + "3": "Stemming 3", + "4": "Stemming 4", + "5": "Stemming 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Gezondheid", + "humidity": "Vochtigheid", + "sleep": "Slapen", + "work": "Werk" + }, "tuya__ipc_work_mode": { "0": "Energiezuinige modus", "1": "Continue werkmodus:" @@ -50,16 +98,34 @@ "power_on": "Aan" }, "tuya__vacuum_cistern": { + "closed": "Gesloten", + "high": "Hoog", "low": "Laag", "middle": "Midden" }, "tuya__vacuum_collection": { + "large": "Groot", "middle": "Midden", "small": "Klein" }, "tuya__vacuum_mode": { + "bow": "Boog", + "chargego": "Keer terug naar dock", + "left_bow": "Boog links", + "left_spiral": "Spiraal links", + "mop": "Dweil", + "part": "Deel", + "partial_bow": "Boog gedeeltelijk", + "pick_zone": "Kies zone", "point": "Punt", "pose": "Houding", + "random": "Willekeurig", + "right_bow": "Boog rechts", + "right_spiral": "Spiraal Rechts", + "single": "Enkel", + "smart": "Smart", + "spiral": "Spiraal", + "standby": "Stand-by", "wall_follow": "Volg muur", "zone": "Zone" } diff --git a/homeassistant/components/tuya/translations/select.no.json b/homeassistant/components/tuya/translations/select.no.json index 03137f3e02804..57b5dc30d48fe 100644 --- a/homeassistant/components/tuya/translations/select.no.json +++ b/homeassistant/components/tuya/translations/select.no.json @@ -10,14 +10,62 @@ "1": "Av", "2": "P\u00e5" }, + "tuya__countdown": { + "1h": "1 time", + "2h": "2 timer", + "3h": "3 timer", + "4h": "4 timer", + "5h": "5 timer", + "6h": "6 timer", + "cancel": "Avbryt" + }, + "tuya__curtain_mode": { + "morning": "Morgen", + "night": "Natt" + }, + "tuya__curtain_motor_mode": { + "back": "Tilbake", + "forward": "Framover" + }, "tuya__decibel_sensitivity": { "0": "Lav f\u00f8lsomhet", "1": "H\u00f8y f\u00f8lsomhet" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Trykk", "switch": "Bryter" }, + "tuya__humidifier_level": { + "level_1": "Niv\u00e5 1", + "level_10": "Niv\u00e5 10", + "level_2": "Niv\u00e5 2", + "level_3": "Niv\u00e5 3", + "level_4": "Niv\u00e5 4", + "level_5": "Niv\u00e5 5", + "level_6": "Niv\u00e5 6", + "level_7": "Niv\u00e5 7", + "level_8": "Niv\u00e5 8", + "level_9": "Niv\u00e5 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Stemning 1", + "2": "Stemning 2", + "3": "Stemning 3", + "4": "Stemning 4", + "5": "Stemning 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Helse", + "humidity": "Fuktighet", + "sleep": "Sove", + "work": "Arbeid" + }, "tuya__ipc_work_mode": { "0": "Lav effekt modus", "1": "Kontinuerlig arbeidsmodus" diff --git a/homeassistant/components/tuya/translations/select.pl.json b/homeassistant/components/tuya/translations/select.pl.json index 832b86e2a7ad8..d75bbd8a8be75 100644 --- a/homeassistant/components/tuya/translations/select.pl.json +++ b/homeassistant/components/tuya/translations/select.pl.json @@ -10,14 +10,62 @@ "1": "wy\u0142.", "2": "w\u0142." }, + "tuya__countdown": { + "1h": "1 godzina", + "2h": "2 godziny", + "3h": "3 godziny", + "4h": "4 godziny", + "5h": "5 godzin", + "6h": "6 godzin", + "cancel": "Anuluj" + }, + "tuya__curtain_mode": { + "morning": "Ranek", + "night": "Noc" + }, + "tuya__curtain_motor_mode": { + "back": "Do ty\u0142u", + "forward": "Do przodu" + }, "tuya__decibel_sensitivity": { "0": "Niska czu\u0142o\u015b\u0107", "1": "Wysoka czu\u0142o\u015b\u0107" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Naci\u015bni\u0119cie", "switch": "Prze\u0142\u0105cznik" }, + "tuya__humidifier_level": { + "level_1": "Poziom 1", + "level_10": "Poziom 10", + "level_2": "Poziom 2", + "level_3": "Poziom 3", + "level_4": "Poziom 4", + "level_5": "Poziom 5", + "level_6": "Poziom 6", + "level_7": "Poziom 7", + "level_8": "Poziom 8", + "level_9": "Poziom 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Nastr\u00f3j 1", + "2": "Nastr\u00f3j 2", + "3": "Nastr\u00f3j 3", + "4": "Nastr\u00f3j 4", + "5": "Nastr\u00f3j 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Auto", + "health": "Zdrowotny", + "humidity": "Wilgotno\u015b\u0107", + "sleep": "U\u015bpiony", + "work": "Praca" + }, "tuya__ipc_work_mode": { "0": "Tryb niskiego poboru mocy", "1": "Tryb pracy ci\u0105g\u0142ej" @@ -51,14 +99,14 @@ }, "tuya__vacuum_cistern": { "closed": "zamkni\u0119ta", - "high": "wysoki", - "low": "niski", - "middle": "w po\u0142owie" + "high": "Du\u017ce", + "low": "Ma\u0142e", + "middle": "\u015arednie" }, "tuya__vacuum_collection": { - "large": "du\u017cy", - "middle": "\u015bredni", - "small": "ma\u0142y" + "large": "Du\u017ce", + "middle": "\u015arednie", + "small": "Ma\u0142e" }, "tuya__vacuum_mode": { "bow": "\u0141uk", @@ -66,10 +114,11 @@ "left_bow": "\u0141uk w lewo", "left_spiral": "Spirala w lewo", "mop": "Mop", - "part": "Cz\u0119\u015b\u0107", + "part": "Cz\u0119\u015bciowe", "partial_bow": "Cz\u0119\u015bciowy \u0142uk", "pick_zone": "Wybierz stref\u0119", "point": "Punkt", + "pose": "Pozycja", "random": "Losowo", "right_bow": "\u0141uk w prawo", "right_spiral": "Spirala w prawo", diff --git a/homeassistant/components/tuya/translations/select.pt-BR.json b/homeassistant/components/tuya/translations/select.pt-BR.json index 7d3df1b46aa4a..aed86dfe4ce05 100644 --- a/homeassistant/components/tuya/translations/select.pt-BR.json +++ b/homeassistant/components/tuya/translations/select.pt-BR.json @@ -1,11 +1,133 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Desativado", + "1": "50Hz", + "2": "60Hz" + }, + "tuya__basic_nightvision": { + "0": "Autom\u00e1tico", + "1": "Desligado", + "2": "Ligado" + }, + "tuya__countdown": { + "1h": "1 hora", + "2h": "2 horas", + "3h": "3 horas", + "4h": "4 horas", + "5h": "5 horas", + "6h": "6 horas", + "cancel": "Cancelar" + }, + "tuya__curtain_mode": { + "morning": "Manh\u00e3", + "night": "Noite" + }, + "tuya__curtain_motor_mode": { + "back": "Para tr\u00e1s", + "forward": "Para frente" + }, + "tuya__decibel_sensitivity": { + "0": "Baixa sensibilidade", + "1": "Alta sensibilidade" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__fingerbot_mode": { + "click": "Pulsador", + "switch": "Interruptor" + }, + "tuya__humidifier_level": { + "level_1": "N\u00edvel 1", + "level_10": "N\u00edvel 10", + "level_2": "N\u00edvel 2", + "level_3": "N\u00edvel 3", + "level_4": "N\u00edvel 4", + "level_5": "N\u00edvel 5", + "level_6": "N\u00edvel 6", + "level_7": "N\u00edvel 7", + "level_8": "N\u00edvel 8", + "level_9": "N\u00edvel 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Ambiente 1", + "2": "Ambiente 2", + "3": "Ambiente 3", + "4": "Ambiente 4", + "5": "Ambiente 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Autom\u00e1tico", + "health": "Sa\u00fade", + "humidity": "Umidade", + "sleep": "Sono", + "work": "Trabalho" + }, + "tuya__ipc_work_mode": { + "0": "Modo de baixo consumo", + "1": "Modo de trabalho cont\u00ednuo" + }, + "tuya__led_type": { + "halogen": "Halog\u00eanio", + "incandescent": "Incandescente", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Desligado", + "pos": "Indique a localiza\u00e7\u00e3o do interruptor", + "relay": "Indicar o estado de ligar/desligar" + }, + "tuya__motion_sensitivity": { + "0": "Sensibilidade baixa", + "1": "Sensibilidade m\u00e9dia", + "2": "Sensibilidade alta" + }, + "tuya__record_mode": { + "1": "Gravar apenas eventos", + "2": "Grava\u00e7\u00e3o cont\u00ednua" + }, "tuya__relay_status": { "last": "Lembre-se do \u00faltimo estado", - "memory": "Lembre-se do \u00faltimo estado" + "memory": "Lembre-se do \u00faltimo estado", + "off": "Desligado", + "on": "Ligado", + "power_off": "Desligado", + "power_on": "Ligado" + }, + "tuya__vacuum_cistern": { + "closed": "Fechado", + "high": "Alto", + "low": "Baixo", + "middle": "M\u00e9dio" + }, + "tuya__vacuum_collection": { + "large": "Grande", + "middle": "M\u00e9dio", + "small": "Pequeno" }, "tuya__vacuum_mode": { - "point": "Ponto" + "bow": "", + "chargego": "Retornar para Base", + "left_bow": "", + "left_spiral": "", + "mop": "Esfregar (Mop)", + "part": "Parcial", + "partial_bow": "", + "pick_zone": "C\u00f4modos Selecionados", + "point": "Ponto", + "pose": "Ponto Definido", + "random": "Aleat\u00f3rio", + "right_bow": "", + "right_spiral": "", + "single": "Simples", + "smart": "Autom\u00e1tica", + "spiral": "Espiral", + "standby": "Em Espera", + "wall_follow": "Limpeza de Cantos", + "zone": "\u00c1rea Selecionada" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ru.json b/homeassistant/components/tuya/translations/select.ru.json index b3ab03143d63b..99f7f02771d30 100644 --- a/homeassistant/components/tuya/translations/select.ru.json +++ b/homeassistant/components/tuya/translations/select.ru.json @@ -10,14 +10,53 @@ "1": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "2": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, + "tuya__countdown": { + "1h": "1 \u0447\u0430\u0441", + "2h": "2 \u0447\u0430\u0441\u0430", + "3h": "3 \u0447\u0430\u0441\u0430", + "4h": "4 \u0447\u0430\u0441\u0430", + "5h": "5 \u0447\u0430\u0441\u043e\u0432", + "6h": "6 \u0447\u0430\u0441\u043e\u0432", + "cancel": "\u041e\u0442\u043c\u0435\u043d\u0430" + }, + "tuya__curtain_mode": { + "morning": "\u0423\u0442\u0440\u043e", + "night": "\u041d\u043e\u0447\u044c" + }, + "tuya__curtain_motor_mode": { + "back": "\u041d\u0430\u0437\u0430\u0434", + "forward": "\u0412\u043f\u0435\u0440\u0435\u0434" + }, "tuya__decibel_sensitivity": { "0": "\u041d\u0438\u0437\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", "1": "\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "\u041a\u043d\u043e\u043f\u043a\u0430", "switch": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" }, + "tuya__humidifier_level": { + "level_1": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 1", + "level_10": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 10", + "level_2": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 2", + "level_3": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 3", + "level_4": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 4", + "level_5": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 5", + "level_6": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 6", + "level_7": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 7", + "level_8": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 8", + "level_9": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 9" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "sleep": "\u0421\u043e\u043d", + "work": "\u0420\u0430\u0431\u043e\u0442\u0430" + }, "tuya__ipc_work_mode": { "0": "\u0420\u0435\u0436\u0438\u043c \u043d\u0438\u0437\u043a\u043e\u0433\u043e \u044d\u043d\u0435\u0440\u0433\u043e\u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u044f", "1": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b" @@ -50,6 +89,7 @@ "power_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "tuya__vacuum_cistern": { + "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", "low": "\u041d\u0438\u0437\u043a\u0438\u0439", "middle": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439" @@ -60,18 +100,22 @@ "small": "\u041c\u0430\u043b\u0435\u043d\u044c\u043a\u0438\u0439" }, "tuya__vacuum_mode": { + "bow": "\u041e\u0433\u0438\u0431\u0430\u0442\u044c", "chargego": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c \u043a \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438", + "left_bow": "\u041e\u0433\u0438\u0431\u0430\u0442\u044c \u0441\u043b\u0435\u0432\u0430", "left_spiral": "\u0421\u043f\u0438\u0440\u0430\u043b\u044c \u0432\u043b\u0435\u0432\u043e", "mop": "\u0428\u0432\u0430\u0431\u0440\u0430", "part": "\u0427\u0430\u0441\u0442\u044c", "pick_zone": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0437\u043e\u043d\u0443", "point": "\u0422\u043e\u0447\u043a\u0430", "random": "\u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439", + "right_bow": "\u041e\u0433\u0438\u0431\u0430\u0442\u044c \u0441\u043f\u0440\u0430\u0432\u0430", "right_spiral": "\u0421\u043f\u0438\u0440\u0430\u043b\u044c \u0432\u043f\u0440\u0430\u0432\u043e", "single": "\u041e\u0434\u0438\u043d\u043e\u0447\u043d\u044b\u0439", "smart": "\u0418\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0439", "spiral": "\u0421\u043f\u0438\u0440\u0430\u043b\u044c", "standby": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435", + "wall_follow": "\u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u044c \u0437\u0430 \u0441\u0442\u0435\u043d\u043e\u0439", "zone": "\u0417\u043e\u043d\u0430" } } diff --git a/homeassistant/components/tuya/translations/select.tr.json b/homeassistant/components/tuya/translations/select.tr.json index ff9cd09fe09e2..8b9f26b27cd66 100644 --- a/homeassistant/components/tuya/translations/select.tr.json +++ b/homeassistant/components/tuya/translations/select.tr.json @@ -10,14 +10,62 @@ "1": "Kapal\u0131", "2": "A\u00e7\u0131k" }, + "tuya__countdown": { + "1h": "1 saat", + "2h": "2 saat", + "3h": "3 saat", + "4h": "4 saat", + "5h": "5 saat", + "6h": "6 saat", + "cancel": "\u0130ptal" + }, + "tuya__curtain_mode": { + "morning": "Sabah", + "night": "Gece" + }, + "tuya__curtain_motor_mode": { + "back": "Geri", + "forward": "\u0130leri" + }, "tuya__decibel_sensitivity": { "0": "D\u00fc\u015f\u00fck hassasiyet", "1": "Y\u00fcksek hassasiyet" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Bildirim", "switch": "Anahtar" }, + "tuya__humidifier_level": { + "level_1": "Seviye 1", + "level_10": "Seviye 10", + "level_2": "Seviye 2", + "level_3": "Seviye 3", + "level_4": "Seviye 4", + "level_5": "Seviye 5", + "level_6": "Seviye 6", + "level_7": "Seviye 7", + "level_8": "Seviye 8", + "level_9": "Seviye 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Mod 1", + "2": "Mod 2", + "3": "Mod 3", + "4": "Mod 4", + "5": "Mod 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "Otomatik", + "health": "Sa\u011fl\u0131k", + "humidity": "Nem", + "sleep": "Uyku", + "work": "\u0130\u015f" + }, "tuya__ipc_work_mode": { "0": "D\u00fc\u015f\u00fck g\u00fc\u00e7 modu", "1": "S\u00fcrekli \u00e7al\u0131\u015fma modu" diff --git a/homeassistant/components/tuya/translations/select.uk.json b/homeassistant/components/tuya/translations/select.uk.json new file mode 100644 index 0000000000000..3e802b0e97f63 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.uk.json @@ -0,0 +1,17 @@ +{ + "state": { + "tuya__curtain_mode": { + "morning": "\u0420\u0430\u043d\u043e\u043a", + "night": "\u041d\u0456\u0447" + }, + "tuya__curtain_motor_mode": { + "back": "\u041d\u0430\u0437\u0430\u0434", + "forward": "\u0412\u043f\u0435\u0440\u0435\u0434" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.zh-Hant.json b/homeassistant/components/tuya/translations/select.zh-Hant.json index 8a39fe53b0785..ae835e6db3819 100644 --- a/homeassistant/components/tuya/translations/select.zh-Hant.json +++ b/homeassistant/components/tuya/translations/select.zh-Hant.json @@ -10,14 +10,62 @@ "1": "\u95dc\u9589", "2": "\u958b\u555f" }, + "tuya__countdown": { + "1h": "1 \u5c0f\u6642", + "2h": "2 \u5c0f\u6642", + "3h": "3 \u5c0f\u6642", + "4h": "4 \u5c0f\u6642", + "5h": "5 \u5c0f\u6642", + "6h": "6 \u5c0f\u6642", + "cancel": "\u53d6\u6d88" + }, + "tuya__curtain_mode": { + "morning": "\u65e9\u6668", + "night": "\u591c\u9593" + }, + "tuya__curtain_motor_mode": { + "back": "\u5f8c\u9000", + "forward": "\u524d\u9032" + }, "tuya__decibel_sensitivity": { "0": "\u4f4e\u654f\u611f\u5ea6", "1": "\u9ad8\u654f\u611f\u5ea6" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "\u63a8", "switch": "\u958b\u95dc" }, + "tuya__humidifier_level": { + "level_1": "\u7b49\u7d1a 1", + "level_10": "\u7b49\u7d1a 10", + "level_2": "\u7b49\u7d1a 2", + "level_3": "\u7b49\u7d1a 3", + "level_4": "\u7b49\u7d1a 4", + "level_5": "\u7b49\u7d1a 5", + "level_6": "\u7b49\u7d1a 6", + "level_7": "\u7b49\u7d1a 7", + "level_8": "\u7b49\u7d1a 8", + "level_9": "\u7b49\u7d1a 9" + }, + "tuya__humidifier_moodlighting": { + "1": "\u5fc3\u60c5\u60c5\u5883 1", + "2": "\u5fc3\u60c5\u60c5\u5883 2", + "3": "\u5fc3\u60c5\u60c5\u5883 3", + "4": "\u5fc3\u60c5\u60c5\u5883 4", + "5": "\u5fc3\u60c5\u60c5\u5883 5" + }, + "tuya__humidifier_spray_mode": { + "auto": "\u81ea\u52d5", + "health": "\u5065\u5eb7", + "humidity": "\u6fd5\u5ea6", + "sleep": "\u7761\u7720", + "work": "\u5de5\u4f5c" + }, "tuya__ipc_work_mode": { "0": "\u4f4e\u529f\u8017\u6a21\u5f0f", "1": "\u6301\u7e8c\u5de5\u4f5c\u6a21\u5f0f" diff --git a/homeassistant/components/tuya/translations/sensor.ca.json b/homeassistant/components/tuya/translations/sensor.ca.json index 681ae04107ad6..d5ecb8c52ab31 100644 --- a/homeassistant/components/tuya/translations/sensor.ca.json +++ b/homeassistant/components/tuya/translations/sensor.ca.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bo", + "great": "Genial", + "mild": "Mitj\u00e0", + "severe": "Sever" + }, "tuya__status": { "boiling_temp": "Temperatura d'ebullici\u00f3", "cooling": "Refredant", diff --git a/homeassistant/components/tuya/translations/sensor.de.json b/homeassistant/components/tuya/translations/sensor.de.json index ffe5ddd2c99bd..01daca76a1168 100644 --- a/homeassistant/components/tuya/translations/sensor.de.json +++ b/homeassistant/components/tuya/translations/sensor.de.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Gut", + "great": "Gro\u00dfartig", + "mild": "Mild", + "severe": "Stark" + }, "tuya__status": { "boiling_temp": "Siedetemperatur", "cooling": "K\u00fchlung", diff --git a/homeassistant/components/tuya/translations/sensor.el.json b/homeassistant/components/tuya/translations/sensor.el.json new file mode 100644 index 0000000000000..24f5e675ceb0e --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.el.json @@ -0,0 +1,21 @@ +{ + "state": { + "tuya__air_quality": { + "good": "\u039a\u03b1\u03bb\u03ae", + "great": "\u0395\u03be\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae", + "mild": "\u0389\u03c0\u03b9\u03b1", + "severe": "\u03a3\u03bf\u03b2\u03b1\u03c1\u03ae" + }, + "tuya__status": { + "boiling_temp": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03b2\u03c1\u03b1\u03c3\u03bc\u03bf\u03cd", + "cooling": "\u03a8\u03cd\u03be\u03b7", + "heating": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7", + "heating_temp": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", + "reserve_1": "\u039a\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 1", + "reserve_2": "\u039a\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 2", + "reserve_3": "\u039a\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 3", + "standby": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2", + "warm": "\u0394\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.en.json b/homeassistant/components/tuya/translations/sensor.en.json index 4057f75c1eaf0..fda8003d3e756 100644 --- a/homeassistant/components/tuya/translations/sensor.en.json +++ b/homeassistant/components/tuya/translations/sensor.en.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Good", + "great": "Great", + "mild": "Mild", + "severe": "Severe" + }, "tuya__status": { "boiling_temp": "Boiling temperature", "cooling": "Cooling", diff --git a/homeassistant/components/tuya/translations/sensor.es.json b/homeassistant/components/tuya/translations/sensor.es.json index d625d4504c3c2..7dad02bdf7d74 100644 --- a/homeassistant/components/tuya/translations/sensor.es.json +++ b/homeassistant/components/tuya/translations/sensor.es.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bueno", + "great": "Genial", + "mild": "Moderado", + "severe": "Severo" + }, "tuya__status": { "boiling_temp": "Temperatura de ebullici\u00f3n", "cooling": "Enfriamiento", diff --git a/homeassistant/components/tuya/translations/sensor.et.json b/homeassistant/components/tuya/translations/sensor.et.json index 7e59f93c77eca..a5588a7f0d515 100644 --- a/homeassistant/components/tuya/translations/sensor.et.json +++ b/homeassistant/components/tuya/translations/sensor.et.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Hea", + "great": "Suurep\u00e4rane", + "mild": "Talutav", + "severe": "Ohtlik" + }, "tuya__status": { "boiling_temp": "Keemistemperatuur", "cooling": "Jahutamine", diff --git a/homeassistant/components/tuya/translations/sensor.fr.json b/homeassistant/components/tuya/translations/sensor.fr.json index 795e4c5d43ec9..7041d3f27c665 100644 --- a/homeassistant/components/tuya/translations/sensor.fr.json +++ b/homeassistant/components/tuya/translations/sensor.fr.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bon", + "great": "Super", + "mild": "B\u00e9nin", + "severe": "S\u00e9v\u00e8re" + }, "tuya__status": { "boiling_temp": "Temp\u00e9rature de chauffage", "cooling": "Refroidissement", diff --git a/homeassistant/components/tuya/translations/sensor.he.json b/homeassistant/components/tuya/translations/sensor.he.json new file mode 100644 index 0000000000000..73e5af2fc7937 --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "tuya__air_quality": { + "good": "\u05d8\u05d5\u05d1", + "great": "\u05e0\u05d4\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.hu.json b/homeassistant/components/tuya/translations/sensor.hu.json index 91cd93d627e6c..743c7ffc97e70 100644 --- a/homeassistant/components/tuya/translations/sensor.hu.json +++ b/homeassistant/components/tuya/translations/sensor.hu.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "J\u00f3", + "great": "Nagyszer\u0171", + "mild": "Enyhe", + "severe": "S\u00falyos" + }, "tuya__status": { "boiling_temp": "Forral\u00e1si h\u0151m\u00e9rs\u00e9klet", "cooling": "H\u0171t\u00e9s", diff --git a/homeassistant/components/tuya/translations/sensor.id.json b/homeassistant/components/tuya/translations/sensor.id.json index 0697075be00d9..c841d5faf3bd2 100644 --- a/homeassistant/components/tuya/translations/sensor.id.json +++ b/homeassistant/components/tuya/translations/sensor.id.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bagus", + "great": "Hebat", + "mild": "Ringan", + "severe": "Parah" + }, "tuya__status": { "boiling_temp": "Suhu mendidih", "cooling": "Mendinginkan", diff --git a/homeassistant/components/tuya/translations/sensor.it.json b/homeassistant/components/tuya/translations/sensor.it.json index a7b7bb272dda3..f8cc5ffcb5756 100644 --- a/homeassistant/components/tuya/translations/sensor.it.json +++ b/homeassistant/components/tuya/translations/sensor.it.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Buona", + "great": "Grande", + "mild": "Lieve", + "severe": "Forte" + }, "tuya__status": { "boiling_temp": "Temperatura di ebollizione", "cooling": "Raffreddamento", diff --git a/homeassistant/components/tuya/translations/sensor.ja.json b/homeassistant/components/tuya/translations/sensor.ja.json index aecb556cf173a..317a276315216 100644 --- a/homeassistant/components/tuya/translations/sensor.ja.json +++ b/homeassistant/components/tuya/translations/sensor.ja.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "\u30b0\u30c3\u30c9", + "great": "\u30b0\u30ec\u30fc\u30c8", + "mild": "\u30de\u30a4\u30eb\u30c9", + "severe": "\u30b7\u30d3\u30a2" + }, "tuya__status": { "boiling_temp": "\u6cb8\u70b9", "cooling": "\u51b7\u5374", diff --git a/homeassistant/components/tuya/translations/sensor.nl.json b/homeassistant/components/tuya/translations/sensor.nl.json index 68092c434a3f8..256989b83dcdf 100644 --- a/homeassistant/components/tuya/translations/sensor.nl.json +++ b/homeassistant/components/tuya/translations/sensor.nl.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Goed", + "great": "Geweldig", + "mild": "Mild", + "severe": "Ernstig" + }, "tuya__status": { "boiling_temp": "Kooktemperatuur", "cooling": "Koeling", diff --git a/homeassistant/components/tuya/translations/sensor.no.json b/homeassistant/components/tuya/translations/sensor.no.json index 2992fcb2f4be7..daf17a266f22c 100644 --- a/homeassistant/components/tuya/translations/sensor.no.json +++ b/homeassistant/components/tuya/translations/sensor.no.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bra", + "great": "Utmerket", + "mild": "Mild", + "severe": "Alvorlig" + }, "tuya__status": { "boiling_temp": "Kokende temperatur", "cooling": "Kj\u00f8ling", diff --git a/homeassistant/components/tuya/translations/sensor.pl.json b/homeassistant/components/tuya/translations/sensor.pl.json index 090849227f88d..0529ebebba0df 100644 --- a/homeassistant/components/tuya/translations/sensor.pl.json +++ b/homeassistant/components/tuya/translations/sensor.pl.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Dobra", + "great": "\u015awietna", + "mild": "Umiarkowana", + "severe": "Z\u0142a" + }, "tuya__status": { "boiling_temp": "temperatura wrzenia", "cooling": "ch\u0142odzenie", diff --git a/homeassistant/components/tuya/translations/sensor.pt-BR.json b/homeassistant/components/tuya/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..eaf2621ff142e --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.pt-BR.json @@ -0,0 +1,21 @@ +{ + "state": { + "tuya__air_quality": { + "good": "Bom", + "great": "\u00d3timo", + "mild": "Moderada", + "severe": "Grave" + }, + "tuya__status": { + "boiling_temp": "Temperatura de ebuli\u00e7\u00e3o", + "cooling": "Resfriamento", + "heating": "Aquecimento", + "heating_temp": "Temperatura de aquecimento", + "reserve_1": "Reserva 1", + "reserve_2": "Reserva 2", + "reserve_3": "Reserva 3", + "standby": "Em espera", + "warm": "Preserva\u00e7\u00e3o do calor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.ru.json b/homeassistant/components/tuya/translations/sensor.ru.json index 617237c87683d..9ec7eac14c0a2 100644 --- a/homeassistant/components/tuya/translations/sensor.ru.json +++ b/homeassistant/components/tuya/translations/sensor.ru.json @@ -1,5 +1,10 @@ { "state": { + "tuya__air_quality": { + "good": "\u0425\u043e\u0440\u043e\u0448\u0435\u0435", + "great": "\u041e\u0442\u043b\u0438\u0447\u043d\u043e\u0435", + "mild": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0435" + }, "tuya__status": { "boiling_temp": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043a\u0438\u043f\u0435\u043d\u0438\u044f", "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/tuya/translations/sensor.tr.json b/homeassistant/components/tuya/translations/sensor.tr.json index 3a3088f51f54c..c8e9954660ddb 100644 --- a/homeassistant/components/tuya/translations/sensor.tr.json +++ b/homeassistant/components/tuya/translations/sensor.tr.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "\u0130yi", + "great": "B\u00fcy\u00fck", + "mild": "Hafif", + "severe": "\u015eiddetli" + }, "tuya__status": { "boiling_temp": "Kaynama s\u0131cakl\u0131\u011f\u0131", "cooling": "So\u011futma", diff --git a/homeassistant/components/tuya/translations/sensor.zh-Hant.json b/homeassistant/components/tuya/translations/sensor.zh-Hant.json index 1fd1c2b4d9807..d71fbe849e00a 100644 --- a/homeassistant/components/tuya/translations/sensor.zh-Hant.json +++ b/homeassistant/components/tuya/translations/sensor.zh-Hant.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "\u826f\u597d", + "great": "\u6975\u4f73", + "mild": "\u8f15\u5fae", + "severe": "\u56b4\u91cd" + }, "tuya__status": { "boiling_temp": "\u6cb8\u9a30\u6eab\u5ea6", "cooling": "\u51b7\u6c23", diff --git a/homeassistant/components/tuya/translations/sk.json b/homeassistant/components/tuya/translations/sk.json index 2724fad689873..23d8822116cc0 100644 --- a/homeassistant/components/tuya/translations/sk.json +++ b/homeassistant/components/tuya/translations/sk.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, "step": { + "login": { + "data": { + "country_code": "K\u00f3d krajiny" + } + }, "user": { "data": { + "country_code": "Krajina", "region": "Oblas\u0165" } } diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json index 1d2709d260a0c..97616e5f388c9 100644 --- a/homeassistant/components/tuya/translations/uk.json +++ b/homeassistant/components/tuya/translations/uk.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index f99a4781fdcb6..b905eb0c1e39e 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index 2a9a7915e763b..d0b94efe289c9 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -6,5 +6,6 @@ "requirements": ["twentemilieu==0.5.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["twentemilieu"] } diff --git a/homeassistant/components/twentemilieu/translations/el.json b/homeassistant/components/twentemilieu/translations/el.json new file mode 100644 index 0000000000000..f4949d3832da0 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/el.json @@ -0,0 +1,22 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0393\u03c1\u03ac\u03bc\u03bc\u03b1 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd/\u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1", + "house_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "post_code": "\u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Twente Milieu \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae \u03b1\u03c0\u03bf\u03b2\u03bb\u03ae\u03c4\u03c9\u03bd \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03ae \u03c3\u03b1\u03c2.", + "title": "Twente Milieu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/it.json b/homeassistant/components/twentemilieu/translations/it.json index d8d9570d8ca27..a374885e7aa19 100644 --- a/homeassistant/components/twentemilieu/translations/it.json +++ b/homeassistant/components/twentemilieu/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive", + "house_letter": "Lettera aggiuntiva", "house_number": "Numero civico", "post_code": "CAP" }, diff --git a/homeassistant/components/twentemilieu/translations/pt-BR.json b/homeassistant/components/twentemilieu/translations/pt-BR.json index cc71ffde0e8a1..943b46849b6a2 100644 --- a/homeassistant/components/twentemilieu/translations/pt-BR.json +++ b/homeassistant/components/twentemilieu/translations/pt-BR.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha ao conectar", "invalid_address": "Endere\u00e7o n\u00e3o encontrado na \u00e1rea de servi\u00e7o de Twente Milieu." }, "step": { diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index da063698bd345..e71f3181b5547 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,6 +1,6 @@ """Support for Twilio.""" +from aiohttp import web from twilio.rest import Client -from twilio.twiml import TwiML import voluptuous as vol from homeassistant.components import webhook @@ -51,7 +51,7 @@ async def handle_webhook(hass, webhook_id, request): data["webhook_id"] = webhook_id hass.bus.async_fire(RECEIVED_DATA, dict(data)) - return TwiML().to_xml() + return web.Response(text="") async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index f34dc5684c3f9..5c1415bc8fc96 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -6,5 +6,6 @@ "requirements": ["twilio==6.32.0"], "dependencies": ["webhook"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["twilio"] } diff --git a/homeassistant/components/twilio/translations/bg.json b/homeassistant/components/twilio/translations/bg.json index c3defd52d71ab..c1ef00b7b3c80 100644 --- a/homeassistant/components/twilio/translations/bg.json +++ b/homeassistant/components/twilio/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d \u0441 Home Assistant Cloud.", "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/twilio/translations/ca.json b/homeassistant/components/twilio/translations/ca.json index 22c5e00a8e793..c8c1056a81ce0 100644 --- a/homeassistant/components/twilio/translations/ca.json +++ b/homeassistant/components/twilio/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No connectat a Home Assistant Cloud.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json index 73a3f244f9603..97c04631aa5f8 100644 --- a/homeassistant/components/twilio/translations/de.json +++ b/homeassistant/components/twilio/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nicht mit der Home Assistant Cloud verbunden.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." }, diff --git a/homeassistant/components/twilio/translations/el.json b/homeassistant/components/twilio/translations/el.json index aecb2ee553fa4..91b755c797beb 100644 --- a/homeassistant/components/twilio/translations/el.json +++ b/homeassistant/components/twilio/translations/el.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "cloud_not_connected": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant Cloud.", + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", + "webhook_not_internet_accessible": "\u0397 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 webhook." + }, + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03c3\u03c4\u03bf\u03bd Home Assistant, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf [Webhooks with Twilio]( {twilio_url} ). \n\n \u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: \n\n - URL: ` {webhook_url} `\n - \u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2: POST\n - \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae/x-www-form-urlencoded \n\n \u0394\u03b5\u03af\u03c4\u03b5 [\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]( {docs_url} ) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd." + }, + "step": { + "user": { + "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;", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Twilio Webhook" + } } } } \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/en.json b/homeassistant/components/twilio/translations/en.json index 953b807c081d8..2bf509d5ca1b8 100644 --- a/homeassistant/components/twilio/translations/en.json +++ b/homeassistant/components/twilio/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Not connected to Home Assistant Cloud.", "single_instance_allowed": "Already configured. Only a single configuration possible.", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages." }, diff --git a/homeassistant/components/twilio/translations/et.json b/homeassistant/components/twilio/translations/et.json index 3d06e7f1db03d..a8d5ab0ea671c 100644 --- a/homeassistant/components/twilio/translations/et.json +++ b/homeassistant/components/twilio/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pilve\u00fchendus puudub", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, diff --git a/homeassistant/components/twilio/translations/fr.json b/homeassistant/components/twilio/translations/fr.json index cfd4d9813c848..f602994d8b26a 100644 --- a/homeassistant/components/twilio/translations/fr.json +++ b/homeassistant/components/twilio/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/twilio/translations/he.json b/homeassistant/components/twilio/translations/he.json index 7e155c6bdd7a2..4850982aede2c 100644 --- a/homeassistant/components/twilio/translations/he.json +++ b/homeassistant/components/twilio/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "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.", "webhook_not_internet_accessible": "\u05de\u05d5\u05e4\u05e2 \u05d4-Home Assistant \u05e9\u05dc\u05da \u05e6\u05e8\u05d9\u05da \u05dc\u05d4\u05d9\u05d5\u05ea \u05e0\u05d2\u05d9\u05e9 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05d4\u05d5\u05d3\u05e2\u05d5\u05ea webhook." }, diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index 512296463e4e3..409a9b08a720a 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nincs csatlakoztatva a Home Assistant Cloudhoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, diff --git a/homeassistant/components/twilio/translations/id.json b/homeassistant/components/twilio/translations/id.json index be16b1d4802cd..06a77bc974eb5 100644 --- a/homeassistant/components/twilio/translations/id.json +++ b/homeassistant/components/twilio/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Tidak terhubung ke Home Assistant Cloud.", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, diff --git a/homeassistant/components/twilio/translations/it.json b/homeassistant/components/twilio/translations/it.json index 591618bfb6099..8fe5f6316f884 100644 --- a/homeassistant/components/twilio/translations/it.json +++ b/homeassistant/components/twilio/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Non connesso a Home Assistant Cloud.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "webhook_not_internet_accessible": "L'istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi webhook." }, diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 45930c49e7b10..84ea72878e7b4 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, diff --git a/homeassistant/components/twilio/translations/nb.json b/homeassistant/components/twilio/translations/nb.json new file mode 100644 index 0000000000000..d5b8a58a422e0 --- /dev/null +++ b/homeassistant/components/twilio/translations/nb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cloud_not_connected": "Ikke tilkoblet Home Assistant Cloud." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json index 3d31175d2de19..cf180123c989d 100644 --- a/homeassistant/components/twilio/translations/nl.json +++ b/homeassistant/components/twilio/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, diff --git a/homeassistant/components/twilio/translations/no.json b/homeassistant/components/twilio/translations/no.json index 81c5c35e430c8..6e2b57ae8231b 100644 --- a/homeassistant/components/twilio/translations/no.json +++ b/homeassistant/components/twilio/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ikke koblet til Home Assistant Cloud.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, diff --git a/homeassistant/components/twilio/translations/pl.json b/homeassistant/components/twilio/translations/pl.json index e6be0a02aedeb..e0ece3f30421f 100644 --- a/homeassistant/components/twilio/translations/pl.json +++ b/homeassistant/components/twilio/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Brak po\u0142\u0105czenia z chmur\u0105 Home Assistant.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, diff --git a/homeassistant/components/twilio/translations/pt-BR.json b/homeassistant/components/twilio/translations/pt-BR.json index 9c474ca31b73b..4f6e33f0f7a63 100644 --- a/homeassistant/components/twilio/translations/pt-BR.json +++ b/homeassistant/components/twilio/translations/pt-BR.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "cloud_not_connected": "N\u00e3o conectado ao Home Assistant Cloud.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel pela Internet para receber mensagens de webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, voc\u00ea precisar\u00e1 configurar [Webhooks com Twilio] ( {twilio_url} ). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application / x-www-form-urlencoded \n\n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, "step": { "user": { - "description": "Tem certeza de que deseja configurar o Twilio?", + "description": "Deseja iniciar a configura\u00e7\u00e3o?", "title": "Configurar o Twilio Webhook" } } diff --git a/homeassistant/components/twilio/translations/ru.json b/homeassistant/components/twilio/translations/ru.json index 8d255d492a7d3..ae72742d5605b 100644 --- a/homeassistant/components/twilio/translations/ru.json +++ b/homeassistant/components/twilio/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "\u041d\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Home Assistant Cloud.", "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f Webhook-\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439." }, diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json index ef684cbc92c48..fa92c795f25db 100644 --- a/homeassistant/components/twilio/translations/tr.json +++ b/homeassistant/components/twilio/translations/tr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Home Assistant Cloud'a ba\u011fl\u0131 de\u011fil.", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, diff --git a/homeassistant/components/twilio/translations/uk.json b/homeassistant/components/twilio/translations/uk.json index 8ea0ce86a37a0..0ba735ba9c279 100644 --- a/homeassistant/components/twilio/translations/uk.json +++ b/homeassistant/components/twilio/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "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.", "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." }, "create_entry": { diff --git a/homeassistant/components/twilio/translations/zh-Hans.json b/homeassistant/components/twilio/translations/zh-Hans.json index f59a63344036e..f9cf06ceaa65c 100644 --- a/homeassistant/components/twilio/translations/zh-Hans.json +++ b/homeassistant/components/twilio/translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "\u672a\u8fde\u63a5\u81f3 Home Assistant Cloud\u3002" + }, "create_entry": { "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Twilio \u7684 Webhook]({twilio_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" }, diff --git a/homeassistant/components/twilio/translations/zh-Hant.json b/homeassistant/components/twilio/translations/zh-Hant.json index 0776d7cb0e5d8..ae5ddf7549e29 100644 --- a/homeassistant/components/twilio/translations/zh-Hant.json +++ b/homeassistant/components/twilio/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "cloud_not_connected": "\u672a\u9023\u7dda\u81f3 Home Assistant \u96f2\u670d\u52d9\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index 1317bd9a55868..318ecb8304e2b 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/twilio_call", "dependencies": ["twilio"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["twilio"] } diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index c78f5152f13b0..e3b97e9385b59 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -2,9 +2,10 @@ "domain": "twinkly", "name": "Twinkly", "documentation": "https://www.home-assistant.io/integrations/twinkly", - "requirements": ["ttls==1.4.2"], + "requirements": ["ttls==1.4.3"], "codeowners": ["@dr1rrb", "@Robbie1221"], "config_flow": true, "dhcp": [{ "hostname": "twinkly_*" }], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["ttls"] } diff --git a/homeassistant/components/twinkly/translations/el.json b/homeassistant/components/twinkly/translations/el.json index a3847cc08f8f5..8e0294b87b5d9 100644 --- a/homeassistant/components/twinkly/translations/el.json +++ b/homeassistant/components/twinkly/translations/el.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "device_exists": "\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" + }, "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} - {model} ({host});" + }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Twinkly led string \u03c3\u03b1\u03c2", "title": "Twinkly" } diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json index 92171723b5536..c5b01400457fa 100644 --- a/homeassistant/components/twinkly/translations/fr.json +++ b/homeassistant/components/twinkly/translations/fr.json @@ -12,7 +12,7 @@ }, "user": { "data": { - "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly" + "host": "H\u00f4te" }, "description": "Configurer votre Twinkly", "title": "Twinkly" diff --git a/homeassistant/components/twinkly/translations/id.json b/homeassistant/components/twinkly/translations/id.json index 7cebe87febbc2..330bcf6ce11c2 100644 --- a/homeassistant/components/twinkly/translations/id.json +++ b/homeassistant/components/twinkly/translations/id.json @@ -12,7 +12,7 @@ }, "user": { "data": { - "host": "Host (atau alamat IP) perangkat twinkly Anda" + "host": "Host" }, "description": "Siapkan string led Twinkly Anda", "title": "Twinkly" diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json index 4efce28f37f8b..38331177895eb 100644 --- a/homeassistant/components/twinkly/translations/nl.json +++ b/homeassistant/components/twinkly/translations/nl.json @@ -12,7 +12,7 @@ }, "user": { "data": { - "host": "Hostnaam (of IP-adres van uw Twinkly apparaat" + "host": "Host" }, "description": "Uw Twinkly LED-string instellen", "title": "Twinkly" diff --git a/homeassistant/components/twinkly/translations/pt-BR.json b/homeassistant/components/twinkly/translations/pt-BR.json new file mode 100644 index 0000000000000..3aff4eb867d56 --- /dev/null +++ b/homeassistant/components/twinkly/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "device_exists": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "discovery_confirm": { + "description": "Deseja configurar {name} - {model} ( {host} )?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure sua fita de led Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py index 0cdeb8139450b..64feb17d6b5c9 100644 --- a/homeassistant/components/twitch/__init__.py +++ b/homeassistant/components/twitch/__init__.py @@ -1 +1 @@ -"""The twitch component.""" +"""The Twitch component.""" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 706f2d7ab2cc8..ef68ba945187f 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -2,7 +2,8 @@ "domain": "twitch", "name": "Twitch", "documentation": "https://www.home-assistant.io/integrations/twitch", - "requirements": ["python-twitch-client==0.6.0"], + "requirements": ["twitchAPI==2.5.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["twitch"] } diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index b3357d331bdef..771f88f0ef11b 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -3,12 +3,18 @@ import logging -from requests.exceptions import HTTPError -from twitch import TwitchClient +from twitchAPI.twitch import ( + AuthScope, + AuthType, + InvalidTokenException, + MissingScopeException, + Twitch, + TwitchAuthorizationException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,9 +39,12 @@ STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" +OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_TOKEN): cv.string, } @@ -51,28 +60,45 @@ def setup_platform( """Set up the Twitch platform.""" channels = config[CONF_CHANNELS] client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] oauth_token = config.get(CONF_TOKEN) - client = TwitchClient(client_id, oauth_token) + client = Twitch(app_id=client_id, app_secret=client_secret) + client.auto_refresh_auth = False try: - client.ingests.get_server_list() - except HTTPError: - _LOGGER.error("Client ID or OAuth token is not valid") + client.authenticate_app(scope=OAUTH_SCOPES) + except TwitchAuthorizationException: + _LOGGER.error("INvalid client ID or client secret") return - channel_ids = client.users.translate_usernames_to_ids(channels) + if oauth_token: + try: + client.set_user_authentication( + token=oauth_token, scope=OAUTH_SCOPES, validate=True + ) + except MissingScopeException: + _LOGGER.error("OAuth token is missing required scope") + return + except InvalidTokenException: + _LOGGER.error("OAuth token is invalid") + return + + channels = client.get_users(logins=channels) - add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True) + add_entities( + [TwitchSensor(channel=channel, client=client) for channel in channels["data"]], + True, + ) class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" - def __init__(self, channel, client): + def __init__(self, channel, client: Twitch): """Initialize the sensor.""" self._client = client self._channel = channel - self._oauth_enabled = client._oauth_token is not None + self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) self._state = None self._preview = None self._game = None @@ -84,7 +110,7 @@ def __init__(self, channel, client): @property def name(self): """Return the name of the sensor.""" - return self._channel.display_name + return self._channel["display_name"] @property def native_value(self): @@ -101,7 +127,7 @@ def extra_state_attributes(self): """Return the state attributes.""" attr = dict(self._statistics) - if self._oauth_enabled: + if self._enable_user_auth: attr.update(self._subscription) attr.update(self._follow) @@ -112,7 +138,7 @@ def extra_state_attributes(self): @property def unique_id(self): """Return unique ID for this sensor.""" - return self._channel.id + return self._channel["id"] @property def icon(self): @@ -122,41 +148,51 @@ def icon(self): def update(self): """Update device state.""" - channel = self._client.channels.get_by_id(self._channel.id) + followers = self._client.get_users_follows(to_id=self._channel["id"])["total"] + channel = self._client.get_users(user_ids=[self._channel["id"]])["data"][0] self._statistics = { - ATTR_FOLLOWING: channel.followers, - ATTR_VIEWS: channel.views, + ATTR_FOLLOWING: followers, + ATTR_VIEWS: channel["view_count"], } - if self._oauth_enabled: - user = self._client.users.get() + if self._enable_user_auth: + user = self._client.get_users()["data"][0] - try: - sub = self._client.users.check_subscribed_to_channel( - user.id, self._channel.id - ) + subs = self._client.check_user_subscription( + user_id=user["id"], broadcaster_id=self._channel["id"] + ) + if "data" in subs: self._subscription = { ATTR_SUBSCRIPTION: True, - ATTR_SUBSCRIPTION_SINCE: sub.created_at, - ATTR_SUBSCRIPTION_GIFTED: sub.is_gift, + ATTR_SUBSCRIPTION_GIFTED: subs["data"][0]["is_gift"], } - except HTTPError: + elif "status" in subs and subs["status"] == 404: self._subscription = {ATTR_SUBSCRIPTION: False} - - try: - follow = self._client.users.check_follows_channel( - user.id, self._channel.id + elif "error" in subs: + raise Exception( + f"Error response on check_user_subscription: {subs['error']}" ) - self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at} - except HTTPError: + else: + raise Exception("Unknown error response on check_user_subscription") + + follows = self._client.get_users_follows( + from_id=user["id"], to_id=self._channel["id"] + )["data"] + if len(follows) > 0: + self._follow = { + ATTR_FOLLOW: True, + ATTR_FOLLOW_SINCE: follows[0]["followed_at"], + } + else: self._follow = {ATTR_FOLLOW: False} - stream = self._client.streams.get_stream_by_user(self._channel.id) - if stream: - self._game = stream.channel.get("game") - self._title = stream.channel.get("status") - self._preview = stream.preview.get("medium") + streams = self._client.get_streams(user_id=[self._channel["id"]])["data"] + if len(streams) > 0: + stream = streams[0] + self._game = stream["game_name"] + self._title = stream["title"] + self._preview = stream["thumbnail_url"] self._state = STATE_STREAMING else: - self._preview = self._channel.logo + self._preview = channel["offline_image_url"] self._state = STATE_OFFLINE diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index ffd42b8b0fe6b..4e80eef60218f 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": ["TwitterAPI==2.7.5"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["TwitterAPI"] } diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index af19bd68a0617..83953b81d53ec 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": ["openwrt-ubus-rpc==0.0.2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["openwrt"] } diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 5ee2023ce93f8..be59a25f69f3d 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -26,6 +26,7 @@ from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -34,7 +35,7 @@ CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -397,7 +398,9 @@ def _async_check_for_stale(self, *_) -> None: del self._heartbeat_time[unique_id] @staticmethod - async def async_config_entry_updated(hass, config_entry) -> None: + async def async_config_entry_updated( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: """Handle signals of config entry being updated. If config entry is updated due to reauth flow diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b241e07fc8945..60ea4b3284be6 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -198,7 +198,6 @@ def async_update_callback(self) -> None: elif ( self.client.last_updated == SOURCE_DATA - and self._last_seen != self.client.last_seen and self.is_wired == self.client.is_wired ): self._last_seen = self.client.last_seen diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 71e546879b02d..79c8453431d06 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ - "aiounifi==30" + "aiounifi==31" ], "codeowners": [ "@Kane610" @@ -24,5 +24,6 @@ "modelDescription": "UniFi Dream Machine SE" } ], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiounifi"] } \ No newline at end of file diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index faa3d3a22f7c7..9151c81543a38 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -205,6 +205,7 @@ def add_outlet_entities(controller, async_add_entities, devices): or not (device := controller.api.devices[mac]).outlet_table ): continue + for outlet in device.outlets.values(): if outlet.has_relay: switches.append(UniFiOutletSwitch(device, controller, outlet.index)) diff --git a/homeassistant/components/unifi/translations/el.json b/homeassistant/components/unifi/translations/el.json index a0711b5b506ef..b017910c2794b 100644 --- a/homeassistant/components/unifi/translations/el.json +++ b/homeassistant/components/unifi/translations/el.json @@ -2,11 +2,25 @@ "config": { "abort": { "already_configured": "\u039f \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 UniFi Network \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "configuration_updated": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5." + "configuration_updated": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5.", + "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": { + "faulty_credentials": "\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", + "service_unavailable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown_client_mac": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\u03c2 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC" }, "flow_title": "{site} ({host})", "step": { "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "site": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2", + "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" + }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi" } } @@ -15,22 +29,40 @@ "step": { "client_control": { "data": { + "block_client": "\u03a0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 \u03bc\u03b5 \u03b5\u03bb\u03b5\u03b3\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "dpi_restrictions": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03c9\u03bd \u03bf\u03bc\u03ac\u03b4\u03c9\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd DPI", "poe_clients": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 POE \u03c4\u03c9\u03bd \u03c0\u03b5\u03bb\u03b1\u03c4\u03ce\u03bd" - } + }, + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae-\u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\n\n\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03bf\u03c0\u03bf\u03af\u03bf\u03c5\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi 2/3" }, "device_tracker": { "data": { + "detection_time": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b5\u03b8\u03b5\u03ac\u03b8\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03b8\u03b5\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7", "ignore_wired_bug": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03bf\u03b3\u03b9\u03ba\u03ae\u03c2 \u03b5\u03bd\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03c9\u03bd \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi", - "ssid_filter": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 SSID \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2-\u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2" + "ssid_filter": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 SSID \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ad\u03c2-\u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2", + "track_clients": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c7\u03c1\u03b7\u03c3\u03c4\u03ce\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "track_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Ubiquiti)", + "track_wired_clients": "\u03a3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 \u03b5\u03bd\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi 1/3" + }, + "simple_options": { + "data": { + "block_client": "\u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bc\u03b5 \u03b5\u03bb\u03b5\u03b3\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "track_clients": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c7\u03c1\u03b7\u03c3\u03c4\u03ce\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "track_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Ubiquiti)" }, - "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi" }, "statistics_sensors": { "data": { + "allow_bandwidth_sensors": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03b5\u03cd\u03c1\u03bf\u03c5\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", "allow_uptime_sensors": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b5\u03c7\u03bf\u03cd\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" }, - "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd" + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/translations/pt-BR.json b/homeassistant/components/unifi/translations/pt-BR.json index 67b39f07f66a0..0e5ba9af21797 100644 --- a/homeassistant/components/unifi/translations/pt-BR.json +++ b/homeassistant/components/unifi/translations/pt-BR.json @@ -1,22 +1,25 @@ { "config": { "abort": { - "already_configured": "O site de controle j\u00e1 est\u00e1 configurado" + "already_configured": "A conta j\u00e1 foi configurada", + "configuration_updated": "Configura\u00e7\u00e3o atualizada.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas", - "service_unavailable": "Servi\u00e7o indispon\u00edvel", + "faulty_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida", + "service_unavailable": "Falha ao conectar", "unknown_client_mac": "Nenhum cliente dispon\u00edvel nesse endere\u00e7o MAC" }, + "flow_title": "{site} ( {host} )", "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha", "port": "Porta", "site": "ID do site", "username": "Usu\u00e1rio", - "verify_ssl": "Controlador usando certificado apropriado" + "verify_ssl": "Verifique o certificado SSL" }, "title": "Configurar o Controlador UniFi" } @@ -26,27 +29,40 @@ "step": { "client_control": { "data": { - "block_client": "Clientes com acesso controlado \u00e0 rede" + "block_client": "Clientes com acesso controlado \u00e0 rede", + "dpi_restrictions": "Permitir o controle de grupos de restri\u00e7\u00e3o de DPI", + "poe_clients": "Permitir o controle POE de clientes" }, "description": "Configurar controles do cliente \n\nCrie comutadores para os n\u00fameros de s\u00e9rie para os quais deseja controlar o acesso \u00e0 rede.", - "title": "UniFi op\u00e7\u00f5es de 2/3" + "title": "Op\u00e7\u00f5es UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", + "ignore_wired_bug": "Desativar `wired bug logic`", + "ssid_filter": "Selecione SSIDs para rastrear clientes sem fio", "track_clients": "Rastrear clientes da rede", "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" - } - }, - "init": { - "data": { - "one": "um", - "other": "uns" - } + }, + "description": "Configurar rastreamento de dispositivo", + "title": "Op\u00e7\u00f5es UniFi 1/3" }, "simple_options": { + "data": { + "block_client": "Clientes com acesso controlado \u00e0 rede", + "track_clients": "Rastrear clientes da rede", + "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)" + }, "description": "Configurar integra\u00e7\u00e3o UniFi" + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Sensores de uso de largura de banda para clientes de rede", + "allow_uptime_sensors": "Sensores de tempo de atividade para clientes de rede" + }, + "description": "Configurar sensores de estat\u00edsticas", + "title": "Op\u00e7\u00f5es UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/translations/sk.json b/homeassistant/components/unifi/translations/sk.json new file mode 100644 index 0000000000000..da71ce60d66d6 --- /dev/null +++ b/homeassistant/components/unifi/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "faulty_credentials": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index e901d66acbf24..b3ed7d2ef2fc6 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": ["pexpect==4.6.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pexpect", "ptyprocess"] } diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index 46656e4cb3d10..d0716dcec3a3d 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/unifiled", "codeowners": ["@florisvdk"], "requirements": ["unifiled==0.11"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["unifiled"] } diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 39e255bb71516..3039d5153e5b2 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -82,10 +82,10 @@ async def _async_discovery_handoff(self) -> FlowResult: async_start_discovery(self.hass) return self.async_abort(reason="discovery_started") - async def async_step_discovery( + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> FlowResult: - """Handle discovery.""" + """Handle integration discovery.""" self._discovered_device = discovery_info mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) diff --git a/homeassistant/components/unifiprotect/discovery.py b/homeassistant/components/unifiprotect/discovery.py index 4613aee954dae..537e2fa11216f 100644 --- a/homeassistant/components/unifiprotect/discovery.py +++ b/homeassistant/components/unifiprotect/discovery.py @@ -57,7 +57,7 @@ def async_trigger_discovery( hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=asdict(device), ) ) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 22dec33917d01..5fcead53da2a9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -44,7 +44,10 @@ }, { "macaddress": "265A4C*" - } + }, + { + "macaddress": "74ACB9*" + } ], "ssdp": [ { @@ -59,5 +62,6 @@ "manufacturer": "Ubiquiti Networks", "modelDescription": "UniFi Dream Machine SE" } - ] + ], + "loggers": ["pyunifiprotect", "unifi_discovery"] } diff --git a/homeassistant/components/unifiprotect/translations/bg.json b/homeassistant/components/unifiprotect/translations/bg.json index 4e1eaa0c69580..adc8d7ef6996f 100644 --- a/homeassistant/components/unifiprotect/translations/bg.json +++ b/homeassistant/components/unifiprotect/translations/bg.json @@ -14,7 +14,8 @@ "data": { "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": "\u041e\u0442\u043a\u0440\u0438\u0442 \u0435 UniFi Protect" }, "reauth_confirm": { "data": { @@ -29,7 +30,8 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } + }, + "description": "\u0429\u0435 \u0432\u0438 \u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u043b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b, \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d \u0432\u044a\u0432 \u0432\u0430\u0448\u0430\u0442\u0430 UniFi OS Console, \u0437\u0430 \u0434\u0430 \u0432\u043b\u0435\u0437\u0435\u0442\u0435 \u0441 \u043d\u0435\u0433\u043e. \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u0442\u0435 \u0430\u043a\u0430\u0443\u043d\u0442\u0438 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0432 Ubiquiti Cloud \u043d\u044f\u043c\u0430 \u0434\u0430 \u0440\u0430\u0431\u043e\u0442\u044f\u0442. \u0417\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: {local_user_documentation_url}" } } } diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index aad2455a8828e..70c290590b1f3 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -1,24 +1,43 @@ { "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", "discovery_started": "\u0397 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03be\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5" }, "error": { - "protect_version": "\u0397 \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 v1.20.0. \u0391\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf UniFi Protect \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." + "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", + "protect_version": "\u0397 \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 v1.20.0. \u0391\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf UniFi Protect \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name} ( {ip_address} )", "step": { "discovery_confirm": { + "data": { + "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", + "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" + }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf UniFi Protect" }, "reauth_confirm": { "data": { - "host": "IP/Host \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae UniFi Protect" + "host": "IP/Host \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae UniFi Protect", + "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" }, "title": "UniFi Protect Reauth" }, "user": { + "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", + "port": "\u0398\u03cd\u03c1\u03b1", + "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" + }, "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u039a\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 UniFi OS \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5. \u039f\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 Ubiquiti Cloud \u03b4\u03b5\u03bd \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd. \u0393\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: {local_user_documentation_url}", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 UniFi Protect" } diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index 1ba8c5aa5c6c9..efd0cb529b179 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -15,9 +15,11 @@ "discovery_confirm": { "data": { "password": "Mot de passe", - "username": "Nom d'utilisateur" + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" }, - "description": "Voulez-vous configurer {name} ( {ip_address} )\u00a0?" + "description": "Voulez-vous configurer {name} ({ip_address})? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", + "title": "UniFi Protect d\u00e9couvert" }, "reauth_confirm": { "data": { @@ -36,6 +38,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, + "description": "Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", "title": "Configuration d'UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 4e99ac3e90a2b..b2f8e2541fe58 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "discovery_started": "Proses penemuan dimulai" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,16 +10,20 @@ "protect_version": "Versi minimum yang diperlukan adalah v1.20.0. Tingkatkan UniFi Protect lalu coba lagi.", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" - } + }, + "description": "Ingin menyiapkan {name} ({ip_address})? Anda akan memerlukan pengguna lokal yang dibuat di Konsol OS UniFi Anda untuk masuk. Pengguna Ubiquiti Cloud tidak akan berfungsi. Untuk informasi lebih lanjut: {local_user_documentation_url}", + "title": "UniFi Protect Ditemukan" }, "reauth_confirm": { "data": { + "host": "IP/Host dari Server UniFi Protect", "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" @@ -33,6 +38,7 @@ "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" }, + "description": "Anda akan memerlukan pengguna lokal yang dibuat di Konsol OS UniFi Anda untuk masuk. Pengguna Ubiquiti Cloud tidak akan berfungsi. Untuk informasi lebih lanjut: {local_user_documentation_url}", "title": "Penyiapan UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index 7e46c32c859f7..e4ad1b3f2317b 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -38,6 +38,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, + "description": "UniFi OS\u30b3\u30f3\u30bd\u30fc\u30eb\u3067\u4f5c\u6210\u3057\u305f\u30ed\u30fc\u30ab\u30eb\u30e6\u30fc\u30b6\u30fc\u3067\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002Ubiquiti Cloud Users\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001{local_user_documentation_url} \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "UniFi Protect\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/unifiprotect/translations/nb.json b/homeassistant/components/unifiprotect/translations/nb.json new file mode 100644 index 0000000000000..f605133f204f2 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/nb.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/nl.json b/homeassistant/components/unifiprotect/translations/nl.json index a7417a8e50d50..090624f20094f 100644 --- a/homeassistant/components/unifiprotect/translations/nl.json +++ b/homeassistant/components/unifiprotect/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "discovery_started": "Ontdekking gestart" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,7 @@ "protect_version": "Minimaal vereiste versie is v1.20.0. Upgrade UniFi Protect en probeer het opnieuw.", "unknown": "Onverwachte fout" }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index 9e93a791a3559..9b45080b8f1bc 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -18,7 +18,8 @@ "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" }, - "description": "Vil du konfigurere {name} ( {ip_address} )?" + "description": "Vil du konfigurere {name} ( {ip_address} )? Du trenger en lokal bruker opprettet i UniFi OS-konsollen for \u00e5 logge p\u00e5. Ubiquiti Cloud-brukere vil ikke fungere. For mer informasjon: {local_user_documentation_url}", + "title": "UniFi Protect oppdaget" }, "reauth_confirm": { "data": { @@ -37,6 +38,7 @@ "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" }, + "description": "Du trenger en lokal bruker opprettet i UniFi OS-konsollen for \u00e5 logge p\u00e5. Ubiquiti Cloud-brukere vil ikke fungere. For mer informasjon: {local_user_documentation_url}", "title": "UniFi Protect-oppsett" } } diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 0ea84e9d658f3..724609199899c 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "discovery_started": "Wykrywanie rozpocz\u0119te" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,9 +10,16 @@ "protect_version": "Minimalna wymagana wersja to v1.20.0. Zaktualizuj UniFi Protect, a nast\u0119pnie spr\u00f3buj ponownie.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { - "title": "Odkryto UniFi Protect" + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Czy chcesz skonfigurowa\u0107 {name} ({ip_address})? Aby si\u0119 zalogowa\u0107, b\u0119dziesz potrzebowa\u0107 lokalnego u\u017cytkownika utworzonego w konsoli UniFi OS. U\u017cytkownicy Ubiquiti Cloud nie b\u0119d\u0105 dzia\u0142a\u0107. Wi\u0119cej informacji: {local_user_documentation_url}", + "title": "Wykryto UniFi Protect" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 26523104070d5..1f26b9529983a 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -1,17 +1,58 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "discovery_started": "Descoberta iniciada" }, - "flow_title": "{nome} ({ip_address})", + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "protect_version": "A vers\u00e3o m\u00ednima exigida \u00e9 v1.20.0. Atualize o UniFi Protect e tente novamente.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Senha", "username": "Usu\u00e1rio", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verifique o certificado SSL" + }, + "description": "Deseja configurar {name} ({ip_address})?\nVoc\u00ea precisar\u00e1 de um usu\u00e1rio local criado no console do sistema operacional UniFi para fazer login. Usu\u00e1rios da Ubiquiti Cloud n\u00e3o funcionar\u00e3o. Para mais informa\u00e7\u00f5es: {local_user_documentation_url}", + "title": "Descoberta UniFi Protect" + }, + "reauth_confirm": { + "data": { + "host": "IP/Host do Servidor UniFi Protect", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Reautentica\u00e7\u00e3o UniFi Protect" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + }, + "description": "Voc\u00ea precisar\u00e1 de um usu\u00e1rio local criado no console do sistema operacional UniFi para fazer login. Usu\u00e1rios da Ubiquiti Cloud n\u00e3o funcionar\u00e3o. Para mais informa\u00e7\u00f5es: {local_user_documentation_url}", + "title": "Configura\u00e7\u00e3o do UniFi Protect" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "M\u00e9tricas em tempo real (AVISO: aumenta muito o uso da CPU)", + "disable_rtsp": "Desativar o fluxo RTSP", + "override_connection_host": "Anular o host de conex\u00e3o" }, - "description": "Deseja configurar {name} ({ip_address})?" + "description": "A op\u00e7\u00e3o de m\u00e9tricas em tempo real s\u00f3 deve ser habilitada se voc\u00ea tiver habilitado os sensores de diagn\u00f3stico e quiser que eles sejam atualizados em tempo real. Se n\u00e3o estiver ativado, eles ser\u00e3o atualizados apenas uma vez a cada 15 minutos.", + "title": "Op\u00e7\u00f5es de prote\u00e7\u00e3o UniFi" } } } diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index b4ef19257a299..ea9810962f8f0 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -19,7 +19,7 @@ "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({ip_address})? \u0414\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 UniFi OS \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 Ubiquiti Cloud \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c. \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: {local_user_documentation_url}", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 UniFi Protect" + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UniFi Protect" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/unifiprotect/translations/sk.json b/homeassistant/components/unifiprotect/translations/sk.json new file mode 100644 index 0000000000000..3b59b1ff2134a --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "port": "Port" + } + }, + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json new file mode 100644 index 0000000000000..e2bfaa9118cc4 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "title": "UniFi Protect uppt\u00e4ckt" + }, + "user": { + "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/zh-Hans.json b/homeassistant/components/unifiprotect/translations/zh-Hans.json index abc844c32d9e2..72e385606c90f 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hans.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hans.json @@ -1,7 +1,21 @@ { "config": { "error": { - "protect_version": "\u6240\u9700\u7684\u6700\u4f4e\u7248\u672c\u4e3a v1.20.0\u3002\u8bf7\u5347\u7ea7 UniFi Protect\uff0c\u7136\u540e\u91cd\u8bd5\u3002" + "protect_version": "\u8981\u6c42\u8f6f\u4ef6\u6700\u4f4e\u7248\u672c\u4e3a v1.20.0\u3002\u8bf7\u5347\u7ea7 UniFi Protect \u540e\u91cd\u8bd5\u3002" + }, + "step": { + "discovery_confirm": { + "title": "UniFi Protect \u53d1\u73b0\u670d\u52a1" + }, + "reauth_confirm": { + "data": { + "host": "UniFi Protect \u670d\u52a1\u5668\u4e3b\u673a\u5730\u5740" + }, + "title": "UniFi Protect \u91cd\u9a8c\u8bc1" + }, + "user": { + "title": "UniFi Protect \u914d\u7f6e" + } } }, "options": { @@ -11,7 +25,7 @@ "all_updates": "\u5b9e\u65f6\u6307\u6807\uff08\u8b66\u544a\uff1a\u5c06\u663e\u8457\u589e\u52a0 CPU \u5360\u7528\uff09", "disable_rtsp": "\u7981\u7528 RTSP \u6d41" }, - "description": "\u4ec5\u5f53\u60a8\u542f\u7528\u4e86\u8bca\u65ad\u4f20\u611f\u5668\u5e76\u5e0c\u671b\u5176\u5b9e\u65f6\u66f4\u65b0\u65f6\uff0c\u624d\u5e94\u542f\u7528\u5b9e\u65f6\u6307\u6807\u9009\u9879\u3002\u5982\u679c\u672a\u542f\u7528\uff0c\u5b83\u4eec\u5c06\u6bcf 15 \u5206\u949f\u66f4\u65b0\u4e00\u6b21\u3002", + "description": "\u5f53\u60a8\u542f\u7528\u4e86\u8bca\u65ad\u4f20\u611f\u5668\u5e76\u5e0c\u671b\u5176\u5b9e\u65f6\u66f4\u65b0\u65f6\uff0c\u624d\u5e94\u542f\u7528\u5b9e\u65f6\u6307\u6807\u9009\u9879\u3002\n\u82e5\u672a\u542f\u7528\uff0c\u5219\u6bcf 15 \u5206\u949f\u66f4\u65b0\u4e00\u6b21\u6570\u636e\u3002", "title": "UniFi Protect \u9009\u9879" } } diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index 75b64806dffd0..fd5d68e577f0f 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -5,5 +5,6 @@ "requirements": ["upb_lib==0.4.12"], "codeowners": ["@gwww"], "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["upb_lib"] } diff --git a/homeassistant/components/upb/translations/el.json b/homeassistant/components/upb/translations/el.json new file mode 100644 index 0000000000000..7f2ca3521301d --- /dev/null +++ b/homeassistant/components/upb/translations/el.json @@ -0,0 +1,23 @@ +{ + "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_upb_file": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03ba\u03c5\u03c1\u03bf \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 UPB UPStart, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 (\u03b2\u03bb\u03ad\u03c0\u03b5 \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9)", + "file_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03ba\u03b1\u03b9 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 UPStart UPB.", + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Universal Powerline Bus Powerline Interface Module (UPB PIM). \u0397 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03c9\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'address[:port]' \u03b3\u03b9\u03b1 'tcp'. \u0397 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 2101. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '192.168.1.42'. \u0393\u03b9\u03b1 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf, \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'tty[:baud]'. \u03a4\u03bf baud \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 4800. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '/dev/ttyS1'.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/it.json b/homeassistant/components/upb/translations/it.json index 1de3cdddabdae..e84a9319f755a 100644 --- a/homeassistant/components/upb/translations/it.json +++ b/homeassistant/components/upb/translations/it.json @@ -16,7 +16,7 @@ "protocol": "Protocollo" }, "description": "Collega un Modulo Interfaccia Powerline del Bus Universale Powerline (UPB PIM). La stringa dell'indirizzo deve essere nel formato 'indirizzo[:porta]' per 'tcp'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101. Esempio: '192.168.1.42'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Baud \u00e8 opzionale e il valore predefinito \u00e8 4800. Esempio: '/dev/ttyS1'.", - "title": "Collegamento a UPB PIM" + "title": "Connettiti a UPB PIM" } } } diff --git a/homeassistant/components/upb/translations/pt-BR.json b/homeassistant/components/upb/translations/pt-BR.json index 093611b233140..402b81140f92b 100644 --- a/homeassistant/components/upb/translations/pt-BR.json +++ b/homeassistant/components/upb/translations/pt-BR.json @@ -1,17 +1,22 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "unknown": "Erro inesperado." + "cannot_connect": "Falha ao conectar", + "invalid_upb_file": "Caminho e nome do arquivo de exporta\u00e7\u00e3o UPSstart UPB.", + "unknown": "Erro inesperado" }, "step": { "user": { "data": { "address": "Endere\u00e7o (veja a descri\u00e7\u00e3o acima)", + "file_path": "Caminho e nome do arquivo de exporta\u00e7\u00e3o UPSstart UPB.", "protocol": "Protocolo" - } + }, + "description": "Conecte um M\u00f3dulo de Interface Powerline Universal Powerline Bus (UPB PIM). A string de endere\u00e7o deve estar no formato 'address[:port]' para 'tcp'. A porta \u00e9 opcional e o padr\u00e3o \u00e9 2101. Exemplo: '192.168.1.42'. Para o protocolo serial, o endere\u00e7o deve estar no formato 'tty[:baud]'. O baud \u00e9 opcional e o padr\u00e3o \u00e9 4800. Exemplo: '/dev/ttyS1'.", + "title": "Conecte-se ao UPB PIM" } } } diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 8d5d2c16fbb48..e499404945263 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/upc_connect", "requirements": ["connect-box==0.2.8"], "codeowners": ["@pvizeli", "@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["connect_box"] } diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index a4901dcf2d24b..a824bc596d004 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -21,16 +21,18 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL +from .const import CONFIG_ENTRY_UPDATE_SIGNAL_TEMPLATE, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -177,7 +179,7 @@ class UpCloudServerEntity(CoordinatorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, upcloud_api.Server]], + coordinator: UpCloudDataUpdateCoordinator, uuid: str, ) -> None: """Initialize the UpCloud server entity.""" @@ -235,3 +237,17 @@ def extra_state_attributes(self) -> dict[str, Any]: ATTR_MEMORY_AMOUNT, ) } + + @property + def device_info(self) -> DeviceInfo: + """Return info for device registry.""" + assert self.coordinator.config_entry is not None + return DeviceInfo( + configuration_url="https://hub.upcloud.com", + default_model="Control Panel", + entry_type=DeviceEntryType.SERVICE, + identifiers={ + (DOMAIN, f"{self.coordinator.config_entry.data[CONF_USERNAME]}@hub") + }, + manufacturer="UpCloud Ltd", + ) diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index a9e0f74462e0a..26e1f92ef9ac7 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": ["upcloud-api==2.0.0"], "codeowners": ["@scop"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["upcloud_api"] } diff --git a/homeassistant/components/upcloud/translations/el.json b/homeassistant/components/upcloud/translations/el.json index e906610d5659f..ace40a192a396 100644 --- a/homeassistant/components/upcloud/translations/el.json +++ b/homeassistant/components/upcloud/translations/el.json @@ -1,4 +1,18 @@ { + "config": { + "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" + }, + "step": { + "user": { + "data": { + "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" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/upcloud/translations/pt-BR.json b/homeassistant/components/upcloud/translations/pt-BR.json new file mode 100644 index 0000000000000..0fadb90bf5a8e --- /dev/null +++ b/homeassistant/components/upcloud/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o em segundos, m\u00ednimo 30" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/sk.json b/homeassistant/components/upcloud/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/upcloud/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 6a6264304c9fc..4f88b5d13696a 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -58,16 +58,10 @@ def __init__( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the updater component.""" - conf = config.get(DOMAIN, {}) - - for option in (CONF_COMPONENT_REPORTING, CONF_REPORTING): - if option in conf: - _LOGGER.warning( - "Analytics reporting with the option '%s' " - "is deprecated and you should remove that from your configuration. " - "The analytics part of this integration has moved to the new 'analytics' integration", - option, - ) + _LOGGER.warning( + "The updater integration has been deprecated and will be removed in 2022.5, " + "please remove it from your configuration" + ) async def check_new_version() -> Updater: """Check if a new version is available and report if one is.""" diff --git a/homeassistant/components/updater/translations/pt-BR.json b/homeassistant/components/updater/translations/pt-BR.json index 7d07ec8da096b..cc89a22092af2 100644 --- a/homeassistant/components/updater/translations/pt-BR.json +++ b/homeassistant/components/updater/translations/pt-BR.json @@ -1,3 +1,3 @@ { - "title": "Atualizador" + "title": "Gerenciador de atualiza\u00e7\u00f5es" } \ No newline at end of file diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2ac975ada4aa7..5b630973f67a4 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.23.4"], + "requirements": ["async-upnp-client==0.23.5"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ @@ -14,5 +14,6 @@ "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" } ], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["async_upnp_client"] } diff --git a/homeassistant/components/upnp/translations/el.json b/homeassistant/components/upnp/translations/el.json new file mode 100644 index 0000000000000..9a84ba81ced34 --- /dev/null +++ b/homeassistant/components/upnp/translations/el.json @@ -0,0 +1,31 @@ +{ + "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", + "incomplete_discovery": "\u0395\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\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}", + "step": { + "ssdp_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae UPnP/IGD;" + }, + "user": { + "data": { + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1, \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf 30)", + "unique_id": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "usn": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1, \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf 30)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/pt-BR.json b/homeassistant/components/upnp/translations/pt-BR.json index 325865ba87eab..a1544981ea91e 100644 --- a/homeassistant/components/upnp/translations/pt-BR.json +++ b/homeassistant/components/upnp/translations/pt-BR.json @@ -1,17 +1,31 @@ { "config": { "abort": { - "already_configured": "UPnP / IGD j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "incomplete_discovery": "Descoberta incompleta", - "no_devices_found": "Nenhum dispositivo UPnP/IGD encontrado na rede." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, + "flow_title": "{name}", "step": { + "ssdp_confirm": { + "description": "Deseja configurar este dispositivo UPnP/IGD?" + }, "user": { "data": { "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos, m\u00ednimo 30)", + "unique_id": "Dispositivo", "usn": "Dispositivo" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos, m\u00ednimo 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/sk.json b/homeassistant/components/upnp/translations/sk.json new file mode 100644 index 0000000000000..793f8eff27872 --- /dev/null +++ b/homeassistant/components/upnp/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index c91b08ca12f46..3f08e7e692e51 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -1,6 +1,8 @@ """Config flow for UptimeRobot integration.""" from __future__ import annotations +from typing import Any + from pyuptimerobot import ( UptimeRobot, UptimeRobotAccount, @@ -15,7 +17,6 @@ from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import API_ATTR_OK, DOMAIN, LOGGER @@ -28,7 +29,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 async def _validate_input( - self, data: ConfigType + self, data: dict[str, Any] ) -> tuple[dict[str, str], UptimeRobotAccount | None]: """Validate the user input allows us to connect.""" errors: dict[str, str] = {} @@ -61,7 +62,9 @@ async def _validate_input( return errors, account - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + 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( @@ -79,13 +82,13 @@ async def async_step_user(self, user_input: ConfigType | None = None) -> FlowRes ) async def async_step_reauth( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Return the reauth confirm step.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: ConfigType | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 17241dba19639..de6399c2e9f1a 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -3,12 +3,13 @@ "name": "UptimeRobot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ - "pyuptimerobot==21.11.0" + "pyuptimerobot==22.2.0" ], "codeowners": [ "@ludeeus", "@chemelli74" ], "quality_scale": "platinum", "iot_class": "cloud_polling", - "config_flow": true + "config_flow": true, + "loggers": ["pyuptimerobot"] } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index b9f2b180b4b94..7ade0ad11a5d7 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -1,10 +1,29 @@ { "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", + "reauth_failed_existing": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2, \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03be\u03b1\u03bd\u03ac.", + "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", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "reauth_failed_matching_account": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c0\u03bf\u03c5 \u03b4\u03ce\u03c3\u03b1\u03c4\u03b5 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "reauth_confirm": { - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}." + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}.", + "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" + }, "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}." } } diff --git a/homeassistant/components/uptimerobot/translations/pt-BR.json b/homeassistant/components/uptimerobot/translations/pt-BR.json new file mode 100644 index 0000000000000..0d9bea96b121d --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_failed_existing": "N\u00e3o foi poss\u00edvel atualizar a entrada de configura\u00e7\u00e3o. Remova a integra\u00e7\u00e3o e configure-a novamente.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "reauth_failed_matching_account": "A chave de API fornecida n\u00e3o corresponde ao ID da conta da configura\u00e7\u00e3o existente.", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "description": "Voc\u00ea precisa fornecer uma nova chave de API somente leitura do UptimeRobot", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave da API" + }, + "description": "Voc\u00ea precisa fornecer uma chave de API somente leitura do UptimeRobot" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.bg.json b/homeassistant/components/uptimerobot/translations/sensor.bg.json new file mode 100644 index 0000000000000..41d3c402ef1f3 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.bg.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u0414\u043e\u043b\u0443", + "not_checked_yet": "\u041d\u0435\u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u043d\u043e", + "pause": "\u041f\u0430\u0443\u0437\u0430", + "seems_down": "\u041d\u0435\u0434\u043e\u0441\u0442\u044a\u043f\u043d\u043e \u0437\u0430 \u043c\u043e\u043c\u0435\u043d\u0442\u0430", + "up": "\u041d\u0430\u0433\u043e\u0440\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.ca.json b/homeassistant/components/uptimerobot/translations/sensor.ca.json new file mode 100644 index 0000000000000..448590b7c0f87 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.ca.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Caigut", + "not_checked_yet": "No comprovat", + "pause": "En pausa", + "seems_down": "Sembla caigut", + "up": "Funcionant" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.de.json b/homeassistant/components/uptimerobot/translations/sensor.de.json new file mode 100644 index 0000000000000..032e59b51977f --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.de.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Herab", + "not_checked_yet": "Noch nicht gepr\u00fcft", + "pause": "Pause", + "seems_down": "Scheint unten zu sein", + "up": "Nach oben" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.el.json b/homeassistant/components/uptimerobot/translations/sensor.el.json new file mode 100644 index 0000000000000..cb4f501001714 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.el.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u039a\u03ac\u03c4\u03c9", + "not_checked_yet": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bb\u03b5\u03b3\u03c7\u03b8\u03b5\u03af \u03b1\u03ba\u03cc\u03bc\u03b1", + "pause": "\u03a0\u03b1\u03cd\u03c3\u03b7", + "seems_down": "\u03a6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2", + "up": "\u03a0\u03ac\u03bd\u03c9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.et.json b/homeassistant/components/uptimerobot/translations/sensor.et.json new file mode 100644 index 0000000000000..75ff376daa599 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.et.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u00dchenduseta", + "not_checked_yet": "Pole veel kontrollitud", + "pause": "Paus", + "seems_down": "Tundub kadunud olevat", + "up": "\u00dchendatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.fr.json b/homeassistant/components/uptimerobot/translations/sensor.fr.json new file mode 100644 index 0000000000000..d5d7602e084b1 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.fr.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "En panne", + "not_checked_yet": "Pas encore v\u00e9rifi\u00e9", + "pause": "Pause", + "seems_down": "Semble en panne", + "up": "Allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.he.json b/homeassistant/components/uptimerobot/translations/sensor.he.json new file mode 100644 index 0000000000000..6d9e58dc5ded4 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "up": "\u05dc\u05de\u05e2\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.hu.json b/homeassistant/components/uptimerobot/translations/sensor.hu.json new file mode 100644 index 0000000000000..f4f3c0b246075 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Nem el\u00e9rhet\u0151", + "not_checked_yet": "M\u00e9g nincs ellen\u0151rizve", + "pause": "Sz\u00fcnet", + "seems_down": "Nem el\u00e9rhet\u0151nek t\u0171nik", + "up": "El\u00e9rhet\u0151" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.id.json b/homeassistant/components/uptimerobot/translations/sensor.id.json new file mode 100644 index 0000000000000..9d66fde17f3e5 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Mati", + "not_checked_yet": "Belum diperiksa", + "pause": "Jeda", + "seems_down": "Sepertinya mati", + "up": "Nyala" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.it.json b/homeassistant/components/uptimerobot/translations/sensor.it.json new file mode 100644 index 0000000000000..260ba6fd7a1d5 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.it.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Gi\u00f9", + "not_checked_yet": "Non ancora controllato", + "pause": "Pausa", + "seems_down": "Sembra non funzionante", + "up": "Su" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.ja.json b/homeassistant/components/uptimerobot/translations/sensor.ja.json new file mode 100644 index 0000000000000..e9bccabf73682 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u4e0b", + "not_checked_yet": "\u307e\u3060\u30c1\u30a7\u30c3\u30af\u3057\u3066\u3044\u307e\u305b\u3093", + "pause": "\u4e00\u6642\u505c\u6b62", + "seems_down": "\u4e0b\u304c\u3063\u3066\u3044\u308b\u3088\u3046\u3067\u3059", + "up": "\u4e0a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.nl.json b/homeassistant/components/uptimerobot/translations/sensor.nl.json new file mode 100644 index 0000000000000..8aad2b8b56f55 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.nl.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Offline", + "not_checked_yet": "Nog niet gecontroleerd", + "pause": "Pauzeer", + "seems_down": "Lijkt offline", + "up": "Online" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.no.json b/homeassistant/components/uptimerobot/translations/sensor.no.json new file mode 100644 index 0000000000000..183ba3306d2c8 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.no.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Ned", + "not_checked_yet": "Ikke sjekket enn\u00e5", + "pause": "Pause", + "seems_down": "Virker nede", + "up": "Opp" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.pl.json b/homeassistant/components/uptimerobot/translations/sensor.pl.json new file mode 100644 index 0000000000000..d1002376d8272 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.pl.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "offline", + "not_checked_yet": "jeszcze nie sprawdzone", + "pause": "wstrzymano", + "seems_down": "wydaje si\u0119 offline", + "up": "online" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.pt-BR.json b/homeassistant/components/uptimerobot/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..7bf2a85b968eb --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.pt-BR.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Para baixo", + "not_checked_yet": "Ainda n\u00e3o foi verificado", + "pause": "Pausado", + "seems_down": "Parece baixo", + "up": "Para cima" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.ru.json b/homeassistant/components/uptimerobot/translations/sensor.ru.json new file mode 100644 index 0000000000000..72f6abfd12a62 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.ru.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u041d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442", + "not_checked_yet": "\u0415\u0449\u0451 \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e", + "pause": "\u041f\u0430\u0443\u0437\u0430", + "seems_down": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442", + "up": "\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.tr.json b/homeassistant/components/uptimerobot/translations/sensor.tr.json new file mode 100644 index 0000000000000..b5ee9cfd01f92 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "A\u015fa\u011f\u0131", + "not_checked_yet": "Hen\u00fcz kontrol edilmedi", + "pause": "Duraklat", + "seems_down": "A\u015fa\u011f\u0131 g\u00f6r\u00fcn\u00fcyor", + "up": "Yukar\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.zh-Hant.json b/homeassistant/components/uptimerobot/translations/sensor.zh-Hant.json new file mode 100644 index 0000000000000..5ca514829f59c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.zh-Hant.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "\u96e2\u7dda", + "not_checked_yet": "\u5c1a\u672a\u6aa2\u67e5", + "pause": "\u66ab\u505c", + "seems_down": "\u4f3c\u4e4e\u96e2\u7dda", + "up": "\u7dda\u4e0a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sk.json b/homeassistant/components/uptimerobot/translations/sk.json new file mode 100644 index 0000000000000..a41b646034bef --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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" + } + }, + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 6ae41e340ab13..0680848f70a32 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["uscisstatus"] } diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index d38a5c056b841..9c1f4566dc391 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", "requirements": ["geojson_client==0.6"], "codeowners": ["@exxamalte"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geojson_client"] } diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index a1ba3b6d370a9..fb880f567d1e2 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -5,5 +5,6 @@ "requirements": ["croniter==1.0.6"], "codeowners": ["@dgomes"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["croniter"] } diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 507ee518454a3..99e43c6654ffb 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/uvc", "requirements": ["uvcclient==0.11.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["uvcclient"] } diff --git a/homeassistant/components/vacuum/translations/pt-BR.json b/homeassistant/components/vacuum/translations/pt-BR.json index 77a38f3b29863..e149d944f9338 100644 --- a/homeassistant/components/vacuum/translations/pt-BR.json +++ b/homeassistant/components/vacuum/translations/pt-BR.json @@ -1,5 +1,9 @@ { "device_automation": { + "action_type": { + "clean": "Permitir {entity_name} limpar", + "dock": "Permitir {entity_name} voltar \u00e0 base" + }, "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpando", "is_docked": "{entity_name} est\u00e1 na base" @@ -13,8 +17,8 @@ "_": { "cleaning": "Limpando", "docked": "Na base", - "error": "Erro", - "idle": "Em espera", + "error": "Falha", + "idle": "Ocioso", "off": "Desligado", "on": "Ligado", "paused": "Pausado", diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 8a23f7d8df933..aeb9e59e28682 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from datetime import date import ipaddress import logging from typing import Any, NamedTuple @@ -9,7 +10,10 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api.exceptions import ValloxApiException -from vallox_websocket_api.vallox import get_uuid as calculate_uuid +from vallox_websocket_api.vallox import ( + get_next_filter_change_date as calculate_next_filter_change_date, + get_uuid as calculate_uuid, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -29,7 +33,6 @@ METRIC_KEY_PROFILE_FAN_SPEED_BOOST, METRIC_KEY_PROFILE_FAN_SPEED_HOME, STATE_SCAN_INTERVAL, - STR_TO_VALLOX_PROFILE_SETTABLE, ) _LOGGER = logging.getLogger(__name__) @@ -55,17 +58,8 @@ Platform.BINARY_SENSOR, ] -ATTR_PROFILE = "profile" ATTR_PROFILE_FAN_SPEED = "fan_speed" -SERVICE_SCHEMA_SET_PROFILE = vol.Schema( - { - vol.Required(ATTR_PROFILE): vol.All( - cv.string, vol.In(STR_TO_VALLOX_PROFILE_SETTABLE) - ) - } -) - SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema( { vol.Required(ATTR_PROFILE_FAN_SPEED): vol.All( @@ -82,16 +76,11 @@ class ServiceMethodDetails(NamedTuple): schema: vol.Schema -SERVICE_SET_PROFILE = "set_profile" SERVICE_SET_PROFILE_FAN_SPEED_HOME = "set_profile_fan_speed_home" SERVICE_SET_PROFILE_FAN_SPEED_AWAY = "set_profile_fan_speed_away" SERVICE_SET_PROFILE_FAN_SPEED_BOOST = "set_profile_fan_speed_boost" SERVICE_TO_METHOD = { - SERVICE_SET_PROFILE: ServiceMethodDetails( - method="async_set_profile", - schema=SERVICE_SCHEMA_SET_PROFILE, - ), SERVICE_SET_PROFILE_FAN_SPEED_HOME: ServiceMethodDetails( method="async_set_profile_fan_speed_home", schema=SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, @@ -132,6 +121,15 @@ def get_uuid(self) -> UUID | None: raise ValueError return uuid + def get_next_filter_change_date(self) -> date | None: + """Return the next filter change date.""" + next_filter_change_date = calculate_next_filter_change_date(self.metric_cache) + + if not isinstance(next_filter_change_date, date): + return None + + return next_filter_change_date + class ValloxDataUpdateCoordinator(DataUpdateCoordinator): """The DataUpdateCoordinator for Vallox.""" @@ -229,24 +227,6 @@ def __init__( self._client = client self._coordinator = coordinator - async def async_set_profile(self, profile: str = "Home") -> bool: - """Set the ventilation profile.""" - _LOGGER.debug("Setting ventilation profile to: %s", profile) - - _LOGGER.warning( - "Attention: The service 'vallox.set_profile' is superseded by the " - "'fan.set_preset_mode' service. It will be removed in the future, please migrate to " - "'fan.set_preset_mode' to prevent breakage" - ) - - try: - await self._client.set_profile(STR_TO_VALLOX_PROFILE_SETTABLE[profile]) - return True - - except (OSError, ValloxApiException) as err: - _LOGGER.error("Error setting ventilation profile: %s", err) - return False - async def async_set_profile_fan_speed_home( self, fan_speed: int = DEFAULT_FAN_SPEED_HOME ) -> bool: diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4fb0bd29ac58e..71b0750e2f2a4 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,8 +2,9 @@ "domain": "vallox", "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.9.0"], + "requirements": ["vallox-websocket-api==2.11.0"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["vallox_websocket_api"] } diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 44dfb56fafc92..eece054c82e18 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import datetime, time from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt as dt_util +from homeassistant.util import dt from . import ValloxDataUpdateCoordinator from .const import ( @@ -95,18 +95,15 @@ class ValloxFilterRemainingSensor(ValloxSensor): @property def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" - super_native_value = super().native_value + next_filter_change_date = self.coordinator.data.get_next_filter_change_date() - if not isinstance(super_native_value, (int, float)): + if next_filter_change_date is None: return None - # Since only a delta of days is received from the device, fix the time so the timestamp does - # not change with every update. - days_remaining = float(super_native_value) - days_remaining_delta = timedelta(days=days_remaining) - now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - - return (now + days_remaining_delta).astimezone(dt_util.UTC) + return datetime.combine( + next_filter_change_date, + time(hour=13, minute=0, second=0, tzinfo=dt.DEFAULT_TIME_ZONE), + ) class ValloxCellStateSensor(ValloxSensor): @@ -150,7 +147,6 @@ class ValloxSensorEntityDescription(SensorEntityDescription): ValloxSensorEntityDescription( key="remaining_time_for_filter", name="Remaining Time For Filter", - metric_key="A_CYC_REMAINING_TIME_FOR_FILTER", device_class=SensorDeviceClass.TIMESTAMP, sensor_type=ValloxFilterRemainingSensor, ), diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index 5cfa1dae4b5cc..d6a0ec238c378 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -1,19 +1,3 @@ -set_profile: - name: Set profile - description: Set the ventilation profile. - fields: - profile: - name: Profile - description: "Set profile." - required: true - selector: - select: - options: - - 'Away' - - 'Boost' - - 'Fireplace' - - 'Home' - set_profile_fan_speed_home: name: Set profile fan speed home description: Set the fan speed of the Home profile. diff --git a/homeassistant/components/vallox/translations/cs.json b/homeassistant/components/vallox/translations/cs.json index ccfe59404c386..2a364830dbf9b 100644 --- a/homeassistant/components/vallox/translations/cs.json +++ b/homeassistant/components/vallox/translations/cs.json @@ -1,12 +1,20 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "name": "Jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/el.json b/homeassistant/components/vallox/translations/el.json index a4a16fd34eed0..4b15c9262479f 100644 --- a/homeassistant/components/vallox/translations/el.json +++ b/homeassistant/components/vallox/translations/el.json @@ -1,7 +1,22 @@ { "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_host": "\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", + "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", + "invalid_host": "\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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "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" + }, "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 Vallox. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {integration_docs_url}.", "title": "Vallox" } diff --git a/homeassistant/components/vallox/translations/id.json b/homeassistant/components/vallox/translations/id.json index 45c32845701ef..4f8fbbff6e977 100644 --- a/homeassistant/components/vallox/translations/id.json +++ b/homeassistant/components/vallox/translations/id.json @@ -17,7 +17,7 @@ "host": "Host", "name": "Nama" }, - "description": "Siapkan integrasi Vallox Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/vallox ", + "description": "Siapkan integrasi Vallox Jika Anda memiliki masalah dengan konfigurasi, buka {integration_docs_url}.", "title": "Vallox" } } diff --git a/homeassistant/components/vallox/translations/lv.json b/homeassistant/components/vallox/translations/lv.json new file mode 100644 index 0000000000000..cee9047f155ec --- /dev/null +++ b/homeassistant/components/vallox/translations/lv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Vallox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/pt-BR.json b/homeassistant/components/vallox/translations/pt-BR.json new file mode 100644 index 0000000000000..729e5257db8ed --- /dev/null +++ b/homeassistant/components/vallox/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Configure a integra\u00e7\u00e3o Vallox. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, v\u00e1 para {integration_docs_url} .", + "title": "Vallox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/sk.json b/homeassistant/components/vallox/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/vallox/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index 965e84435db7d..4f4a6a8b4a8be 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "requirements": ["vtjp==0.1.14"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["vasttrafik"] } diff --git a/homeassistant/components/velbus/diagnostics.py b/homeassistant/components/velbus/diagnostics.py new file mode 100644 index 0000000000000..f6015abd1f833 --- /dev/null +++ b/homeassistant/components/velbus/diagnostics.py @@ -0,0 +1,57 @@ +"""Diagnostics support for Velbus.""" +from __future__ import annotations + +from typing import Any + +from velbusaio.channels import Channel as VelbusChannel +from velbusaio.module import Module as VelbusModule + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + controller = hass.data[DOMAIN][entry.entry_id]["cntrl"] + data: dict[str, Any] = {"entry": entry.as_dict(), "modules": []} + for module in controller.get_modules().values(): + data["modules"].append(_build_module_diagnostics_info(module)) + return data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + controller = hass.data[DOMAIN][entry.entry_id]["cntrl"] + channel = list(next(iter(device.identifiers)))[1] + modules = controller.get_modules() + return _build_module_diagnostics_info(modules[int(channel)]) + + +def _build_module_diagnostics_info(module: VelbusModule) -> dict[str, Any]: + """Build per module diagnostics info.""" + data: dict[str, Any] = { + "type": module.get_type_name(), + "address": module.get_addresses(), + "name": module.get_name(), + "sw_version": module.get_sw_version(), + "is_loaded": module.is_loaded(), + "channels": _build_channels_diagnostics_info(module.get_channels()), + } + return data + + +def _build_channels_diagnostics_info( + channels: dict[str, VelbusChannel] +) -> dict[str, Any]: + """Build diagnostics info for all channels.""" + data: dict[str, Any] = {} + for channel in channels.values(): + data[str(channel.get_channel_number())] = channel.get_channel_info() + return data diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index f1a9065171688..2926b30c22f09 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -22,7 +22,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VelbusEntity @@ -96,6 +96,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): _channel: VelbusButton _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.CONFIG _attr_supported_features = SUPPORT_FLASH def __init__(self, channel: VelbusChannel) -> None: diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 3ffb29632e0e2..c9a72aa2d8e78 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.2.1"], + "requirements": ["velbus-aio==2022.2.4"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], @@ -24,5 +24,6 @@ "vid": "10CF", "pid": "0518" } - ] + ], + "loggers": ["velbusaio"] } diff --git a/homeassistant/components/velbus/translations/el.json b/homeassistant/components/velbus/translations/el.json index 04b238a916d22..b50784422b221 100644 --- a/homeassistant/components/velbus/translations/el.json +++ b/homeassistant/components/velbus/translations/el.json @@ -1,7 +1,20 @@ { "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": { + "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" + }, + "step": { + "user": { + "data": { + "name": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 velbus", + "port": "\u03a3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 velbus" + } } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/pt-BR.json b/homeassistant/components/velbus/translations/pt-BR.json new file mode 100644 index 0000000000000..6b6142af49f13 --- /dev/null +++ b/homeassistant/components/velbus/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "name": "O nome para esta conex\u00e3o velbus", + "port": "String de conex\u00e3o" + }, + "title": "Defina o tipo de conex\u00e3o velbus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index c72e25d42eb7c..4a5ea07dc82e7 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": ["pyvlx==0.2.19"], "codeowners": ["@Julius2342"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyvlx"] } diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 6fef7bf5d5790..d9f5b51e0ef8e 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -7,5 +7,6 @@ "venstarcolortouch==0.15" ], "codeowners": ["@garbled1"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["venstarcolortouch"] } diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 68d5bad27d666..824774f3e31ae 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -75,8 +75,7 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[Entity] = [] - sensors = coordinator.client.get_sensor_list() - if not sensors: + if not (sensors := coordinator.client.get_sensor_list()): return for sensor_name in sensors: diff --git a/homeassistant/components/venstar/translations/cs.json b/homeassistant/components/venstar/translations/cs.json new file mode 100644 index 0000000000000..e1bf8e7f45f3c --- /dev/null +++ b/homeassistant/components/venstar/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/el.json b/homeassistant/components/venstar/translations/el.json new file mode 100644 index 0000000000000..f80d57e35c8f4 --- /dev/null +++ b/homeassistant/components/venstar/translations/el.json @@ -0,0 +1,23 @@ +{ + "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" + }, + "step": { + "user": { + "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", + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "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" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 Venstar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/it.json b/homeassistant/components/venstar/translations/it.json index 66b7fac78bdbb..c5ddc019984f9 100644 --- a/homeassistant/components/venstar/translations/it.json +++ b/homeassistant/components/venstar/translations/it.json @@ -16,7 +16,7 @@ "ssl": "Utilizza un certificato SSL", "username": "Nome utente" }, - "title": "Collegati al termostato Venstar" + "title": "Connettiti al termostato Venstar" } } } diff --git a/homeassistant/components/venstar/translations/nb.json b/homeassistant/components/venstar/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/venstar/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pt-BR.json b/homeassistant/components/venstar/translations/pt-BR.json new file mode 100644 index 0000000000000..c22a92e18089b --- /dev/null +++ b/homeassistant/components/venstar/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "pin": "C\u00f3digo PIN", + "ssl": "Usar um certificado SSL", + "username": "Usu\u00e1rio" + }, + "title": "Conecte-se ao termostato Venstar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 2e708a8b206ad..ef293c279bedb 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -177,7 +177,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index b9df7a807e639..658ed7904f4bb 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -2,13 +2,14 @@ from __future__ import annotations from collections import defaultdict +from datetime import datetime from typing import NamedTuple import pyvera as pv from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.event import call_later from .const import DOMAIN @@ -56,7 +57,7 @@ def __init__(self, hass: HomeAssistant) -> None: """Initialize the object.""" super().__init__() self._hass = hass - self._cancel_poll = None + self._cancel_poll: CALLBACK_TYPE | None = None def start(self) -> None: """Start polling for data.""" @@ -72,7 +73,7 @@ def stop(self) -> None: def _schedule_poll(self, delay: float) -> None: self._cancel_poll = call_later(self._hass, delay, self._run_poll_server) - def _run_poll_server(self, now) -> None: + def _run_poll_server(self, now: datetime) -> None: delay = 1 # Long poll for changes. The downstream API instructs the endpoint to wait a diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 84cf9eac007c2..5a87ae29483af 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.13"], "codeowners": ["@pavoni"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyvera"] } diff --git a/homeassistant/components/vera/translations/el.json b/homeassistant/components/vera/translations/el.json new file mode 100644 index 0000000000000..1039fb01ccd53 --- /dev/null +++ b/homeassistant/components/vera/translations/el.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae \u03bc\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Vera \u03c0\u03c1\u03bf\u03c2 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant.", + "lights": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03bc\u03b5\u03c4\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 Vera \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c9\u03c2 \u03c6\u03ce\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant.", + "vera_controller_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae" + }, + "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae Vera \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9. \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bc\u03bf\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc: http://192.168.1.161:3480.", + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd Vera \u03c0\u03c1\u03bf\u03c2 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant.", + "lights": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03c4\u03ce\u03bd Vera \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c9\u03c2 \u03c6\u03ce\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant." + }, + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 vera \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2: https://www.home-assistant.io/integrations/vera/. \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u039f\u03c0\u03bf\u03b9\u03b1\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b5\u03b4\u03ce \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 home assistant server. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2, \u03b4\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03b5\u03bd\u03cc.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/pt-BR.json b/homeassistant/components/vera/translations/pt-BR.json new file mode 100644 index 0000000000000..361b0487617c6 --- /dev/null +++ b/homeassistant/components/vera/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao controlador com URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "IDs de dispositivos Vera a serem exclu\u00eddos do Home Assistant.", + "lights": "Vera - alternar os IDs do dispositivo para tratar como luzes no Home Assistant.", + "vera_controller_url": "URL do controlador" + }, + "description": "Forne\u00e7a um URL do controlador Vera abaixo. Deve ficar assim: http://192.168.1.161:3480.", + "title": "Configurar controlador Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "IDs de dispositivos Vera a serem exclu\u00eddos do Home Assistant.", + "lights": "Vera alternar os IDs do dispositivo para tratar como luzes no Home Assistant." + }, + "description": "Consulte a documenta\u00e7\u00e3o do vera para obter detalhes sobre par\u00e2metros opcionais: https://www.home-assistant.io/integrations/vera/. Nota: Quaisquer altera\u00e7\u00f5es aqui precisar\u00e3o de uma reinicializa\u00e7\u00e3o no servidor do Home Assistant. Para limpar valores, forne\u00e7a um espa\u00e7o.", + "title": "Op\u00e7\u00f5es do controlador Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 0bd04961ec72c..c71be7ee4fcb5 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -10,5 +10,6 @@ "macaddress": "0023C1*" } ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["verisure"] } diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json index c837be37bc345..7d46b4ed96b0f 100644 --- a/homeassistant/components/verisure/translations/el.json +++ b/homeassistant/components/verisure/translations/el.json @@ -1,7 +1,18 @@ { "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", + "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_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": { "installation": { + "data": { + "giid": "\u0395\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, "description": "\u03a4\u03bf Home Assistant \u03b2\u03c1\u03ae\u03ba\u03b5 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Verisure \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 My Pages. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant." }, "reauth_confirm": { @@ -13,7 +24,9 @@ }, "user": { "data": { - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Verisure My Pages." + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Verisure My Pages.", + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/verisure/translations/pt-BR.json b/homeassistant/components/verisure/translations/pt-BR.json new file mode 100644 index 0000000000000..1fc733bfd5580 --- /dev/null +++ b/homeassistant/components/verisure/translations/pt-BR.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "installation": { + "data": { + "giid": "Instala\u00e7\u00e3o" + }, + "description": "O Home Assistant encontrou v\u00e1rias instala\u00e7\u00f5es da Verisure na sua conta do My Pages. Por favor, selecione a instala\u00e7\u00e3o para adicionar ao Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Re-autentique com sua conta Verisure My Pages.", + "email": "Email", + "password": "Senha" + } + }, + "user": { + "data": { + "description": "Fa\u00e7a login com sua conta Verisure My Pages.", + "email": "Email", + "password": "Senha" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "O c\u00f3digo PIN padr\u00e3o n\u00e3o corresponde ao n\u00famero necess\u00e1rio de d\u00edgitos" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "N\u00famero de d\u00edgitos no c\u00f3digo PIN para fechaduras", + "lock_default_code": "C\u00f3digo PIN padr\u00e3o para bloqueios, usado se nenhum for fornecido" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/sk.json b/homeassistant/components/verisure/translations/sk.json new file mode 100644 index 0000000000000..0f898b977eea9 --- /dev/null +++ b/homeassistant/components/verisure/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "email": "Email" + } + }, + "user": { + "data": { + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json index 470177997d040..fee8faeab86ed 100644 --- a/homeassistant/components/versasense/manifest.json +++ b/homeassistant/components/versasense/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/versasense", "codeowners": ["@flamm3blemuff1n"], "requirements": ["pyversasense==0.0.6"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyversasense"] } diff --git a/homeassistant/components/version/binary_sensor.py b/homeassistant/components/version/binary_sensor.py new file mode 100644 index 0000000000000..0e60b1b856cc4 --- /dev/null +++ b/homeassistant/components/version/binary_sensor.py @@ -0,0 +1,61 @@ +"""Binary sensor platform for Version.""" +from __future__ import annotations + +from awesomeversion import AwesomeVersion + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, __version__ as HA_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_SOURCE, DEFAULT_NAME, DOMAIN +from .coordinator import VersionDataUpdateCoordinator +from .entity import VersionEntity + +HA_VERSION_OBJECT = AwesomeVersion(HA_VERSION) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up version binary_sensors.""" + coordinator: VersionDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + if (source := config_entry.data[CONF_SOURCE]) == "local": + return + + if (entity_name := config_entry.data[CONF_NAME]) == DEFAULT_NAME: + entity_name = config_entry.title + + entities: list[VersionBinarySensor] = [ + VersionBinarySensor( + coordinator=coordinator, + entity_description=BinarySensorEntityDescription( + key=str(source), + name=f"{entity_name} Update Available", + device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ) + ] + + async_add_entities(entities) + + +class VersionBinarySensor(VersionEntity, BinarySensorEntity): + """Binary sensor for version entities.""" + + entity_description: BinarySensorEntityDescription + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + version = self.coordinator.version + return version is not None and (version > HA_VERSION_OBJECT) diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py index f37fa1c3da2a4..292b194eea114 100644 --- a/homeassistant/components/version/config_flow.py +++ b/homeassistant/components/version/config_flow.py @@ -26,7 +26,7 @@ DEFAULT_SOURCE, DOMAIN, POSTFIX_CONTAINER_NAME, - SOURCE_DOKCER, + SOURCE_DOCKER, SOURCE_HASSIO, STEP_USER, STEP_VERSION_SOURCE, @@ -171,7 +171,7 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any: if source == SOURCE_HASSIO: data[CONF_SOURCE] = "supervisor" data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS - elif source == SOURCE_DOKCER: + elif source == SOURCE_DOCKER: data[CONF_SOURCE] = "container" data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB else: diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 8f1005961e83f..9f480c25cc534 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -11,7 +11,7 @@ DOMAIN: Final = "version" LOGGER: Final[Logger] = getLogger(__package__) -PLATFORMS: Final[list[Platform]] = [Platform.SENSOR] +PLATFORMS: Final[list[Platform]] = [Platform.BINARY_SENSOR, Platform.SENSOR] UPDATE_COORDINATOR_UPDATE_INTERVAL: Final[timedelta] = timedelta(minutes=5) ENTRY_TYPE_SERVICE: Final = "service" @@ -30,7 +30,7 @@ ATTR_VERSION_SOURCE: Final = CONF_VERSION_SOURCE ATTR_SOURCE: Final = CONF_SOURCE -SOURCE_DOKCER: Final = "docker" # Kept to not break existing configurations +SOURCE_DOCKER: Final = "docker" # Kept to not break existing configurations SOURCE_HASSIO: Final = "hassio" # Kept to not break existing configurations VERSION_SOURCE_DOCKER_HUB: Final = "Docker Hub" diff --git a/homeassistant/components/version/entity.py b/homeassistant/components/version/entity.py new file mode 100644 index 0000000000000..1dcdc23fa9fac --- /dev/null +++ b/homeassistant/components/version/entity.py @@ -0,0 +1,33 @@ +"""Common entity class for Version integration.""" + +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, HOME_ASSISTANT +from .coordinator import VersionDataUpdateCoordinator + + +class VersionEntity(CoordinatorEntity): + """Common entity class for Version integration.""" + + _attr_device_info = DeviceInfo( + name=f"{HOME_ASSISTANT} {DOMAIN.title()}", + identifiers={(HOME_ASSISTANT, DOMAIN)}, + manufacturer=HOME_ASSISTANT, + entry_type=DeviceEntryType.SERVICE, + ) + + coordinator: VersionDataUpdateCoordinator + + def __init__( + self, + coordinator: VersionDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize version entities.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}_{entity_description.key}" + ) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 5a4cd70f4c7a8..fa1410521ae00 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ - "pyhaversion==21.11.1" + "pyhaversion==22.02.0" ], "codeowners": [ "@fabaff", @@ -11,5 +11,6 @@ ], "quality_scale": "internal", "iot_class": "local_push", - "config_flow": true + "config_flow": true, + "loggers": ["pyhaversion"] } \ No newline at end of file diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 8b09d893afdf7..f0583a190685d 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -15,11 +15,8 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_SOURCE, @@ -31,12 +28,12 @@ DEFAULT_NAME, DEFAULT_SOURCE, DOMAIN, - HOME_ASSISTANT, LOGGER, VALID_IMAGES, VALID_SOURCES, ) from .coordinator import VersionDataUpdateCoordinator +from .entity import VersionEntity PLATFORM_SCHEMA: Final[Schema] = SENSOR_PLATFORM_SCHEMA.extend( { @@ -91,30 +88,10 @@ async def async_setup_entry( async_add_entities(version_sensor_entities) -class VersionSensorEntity(CoordinatorEntity, SensorEntity): +class VersionSensorEntity(VersionEntity, SensorEntity): """Version sensor entity class.""" _attr_icon = "mdi:package-up" - _attr_device_info = DeviceInfo( - name=f"{HOME_ASSISTANT} {DOMAIN.title()}", - identifiers={(HOME_ASSISTANT, DOMAIN)}, - manufacturer=HOME_ASSISTANT, - entry_type=DeviceEntryType.SERVICE, - ) - - coordinator: VersionDataUpdateCoordinator - - def __init__( - self, - coordinator: VersionDataUpdateCoordinator, - entity_description: SensorEntityDescription, - ) -> None: - """Initialize version sensor entities.""" - super().__init__(coordinator) - self.entity_description = entity_description - self._attr_unique_id = ( - f"{coordinator.config_entry.entry_id}_{entity_description.key}" - ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/version/translations/el.json b/homeassistant/components/version/translations/el.json index 416c45829026b..4a1a467a41366 100644 --- a/homeassistant/components/version/translations/el.json +++ b/homeassistant/components/version/translations/el.json @@ -1,5 +1,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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/version/translations/ja.json b/homeassistant/components/version/translations/ja.json index a2eba2ffbc0e7..5ae560ddae896 100644 --- a/homeassistant/components/version/translations/ja.json +++ b/homeassistant/components/version/translations/ja.json @@ -16,7 +16,7 @@ "beta": "\u30d9\u30fc\u30bf\u7248\u3092\u542b\u3081\u308b", "board": "\u3069\u306e\u30dc\u30fc\u30c9\u3092\u8ffd\u8de1\u3059\u308b\u304b", "channel": "\u3069\u306e\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u8ffd\u8de1\u3059\u308b\u304b", - "image": "\u3069\u306e\u753b\u50cf\u3092\u8ffd\u8de1\u3059\u308b\u304b" + "image": "\u3069\u306e\u30a4\u30e1\u30fc\u30b8\u3092\u8ffd\u3044\u304b\u3051\u308b\u304b" }, "description": "{version_source} \u30d0\u30fc\u30b8\u30e7\u30f3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u306e\u8a2d\u5b9a", "title": "\u8a2d\u5b9a" diff --git a/homeassistant/components/version/translations/lv.json b/homeassistant/components/version/translations/lv.json new file mode 100644 index 0000000000000..da8048f13fb11 --- /dev/null +++ b/homeassistant/components/version/translations/lv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "version_source": "Versijas avots" + } + }, + "version_source": { + "data": { + "beta": "Iek\u013caut beta versijas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/version/translations/pt-BR.json b/homeassistant/components/version/translations/pt-BR.json new file mode 100644 index 0000000000000..2129822ace56a --- /dev/null +++ b/homeassistant/components/version/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "version_source": "Origem da vers\u00e3o" + }, + "description": "Selecione a fonte da qual voc\u00ea deseja rastrear as vers\u00f5es", + "title": "Selecione o tipo de instala\u00e7\u00e3o" + }, + "version_source": { + "data": { + "beta": "Incluir vers\u00f5es beta", + "board": "Qual placa deve ser rastreada", + "channel": "Qual canal deve ser rastreado", + "image": "Qual imagem deve ser rastreada" + }, + "description": "Configurar o acompanhamento de vers\u00e3o {version_source}", + "title": "Configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 761379f130891..2637cfaa74638 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], "requirements": ["pyvesync==1.4.2"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["pyvesync"] } diff --git a/homeassistant/components/vesync/translations/el.json b/homeassistant/components/vesync/translations/el.json index 22e45ca54fa51..0fd6807133d0f 100644 --- a/homeassistant/components/vesync/translations/el.json +++ b/homeassistant/components/vesync/translations/el.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "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": { + "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": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } } diff --git a/homeassistant/components/vesync/translations/pt-BR.json b/homeassistant/components/vesync/translations/pt-BR.json index c65686007b5d7..89b76484d0ea5 100644 --- a/homeassistant/components/vesync/translations/pt-BR.json +++ b/homeassistant/components/vesync/translations/pt-BR.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o invalida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { + "data": { + "password": "Senha", + "username": "Email" + }, "title": "Digite o nome de usu\u00e1rio e a senha" } } diff --git a/homeassistant/components/vesync/translations/sk.json b/homeassistant/components/vesync/translations/sk.json new file mode 100644 index 0000000000000..c043ef9ff19d2 --- /dev/null +++ b/homeassistant/components/vesync/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/uk.json b/homeassistant/components/vesync/translations/uk.json index 7f6b3a46b1552..1649357f4ff6a 100644 --- a/homeassistant/components/vesync/translations/uk.json +++ b/homeassistant/components/vesync/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." diff --git a/homeassistant/components/vesync/translations/zh-Hant.json b/homeassistant/components/vesync/translations/zh-Hant.json index 264ad237af131..de32d6c787df3 100644 --- a/homeassistant/components/vesync/translations/zh-Hant.json +++ b/homeassistant/components/vesync/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 543b511676097..01cfff593573d 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -205,6 +205,7 @@ def device_info(self): "name": self._device_config.getModel(), "manufacturer": "Viessmann", "model": (DOMAIN, self._device_config.getModel()), + "configuration_url": "https://developer.viessmann.com/", } @property diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index e924a735e0f7f..e1d6bc4223c4f 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -101,6 +101,7 @@ def device_info(self): "name": self._device_config.getModel(), "manufacturer": "Viessmann", "model": (DOMAIN, self._device_config.getModel()), + "configuration_url": "https://developer.viessmann.com/", } @property diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 451ea70edab57..8320db73ad4ea 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -19,6 +19,7 @@ HVAC_MODE_OFF, PRESET_COMFORT, PRESET_ECO, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -87,20 +88,16 @@ VICARE_TO_HA_PRESET_HEATING = { VICARE_PROGRAM_COMFORT: PRESET_COMFORT, VICARE_PROGRAM_ECO: PRESET_ECO, + VICARE_PROGRAM_NORMAL: PRESET_NONE, } HA_TO_VICARE_PRESET_HEATING = { PRESET_COMFORT: VICARE_PROGRAM_COMFORT, PRESET_ECO: VICARE_PROGRAM_ECO, + PRESET_NONE: VICARE_PROGRAM_NORMAL, } -def _build_entity(name, vicare_api, circuit, device_config, heating_type): - """Create a ViCare climate entity.""" - _LOGGER.debug("Found device %s", name) - return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) - - def _get_circuits(vicare_api): """Return the list of circuits.""" try: @@ -126,11 +123,11 @@ async def async_setup_entry( if len(circuits) > 1: suffix = f" {circuit.id}" - entity = _build_entity( + entity = ViCareClimate( f"{name} Heating{suffix}", api, - hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], circuit, + hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG], config_entry.data[CONF_HEATING_TYPE], ) entities.append(entity) @@ -177,6 +174,7 @@ def device_info(self): "name": self._device_config.getModel(), "manufacturer": "Viessmann", "model": (DOMAIN, self._device_config.getModel()), + "configuration_url": "https://developer.viessmann.com/", } def update(self): @@ -328,7 +326,7 @@ def preset_mode(self): @property def preset_modes(self): """Return the available preset mode.""" - return list(VICARE_TO_HA_PRESET_HEATING) + return list(HA_TO_VICARE_PRESET_HEATING) def set_preset_mode(self, preset_mode): """Set new preset mode and deactivate any existing programs.""" @@ -339,8 +337,12 @@ def set_preset_mode(self, preset_mode): ) _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) - self._circuit.deactivateProgram(self._current_program) - self._circuit.activateProgram(vicare_program) + if self._current_program != VICARE_PROGRAM_NORMAL: + # We can't deactivate "normal" + self._circuit.deactivateProgram(self._current_program) + if vicare_program != VICARE_PROGRAM_NORMAL: + # And we can't explicitly activate normal, either + self._circuit.activateProgram(vicare_program) @property def extra_state_attributes(self): diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 98a7eb4c07c1d..3b3058058b6af 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -10,5 +10,6 @@ { "macaddress": "B87424*" } - ] + ], + "loggers": ["PyViCare"] } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 42594ec202e68..249cadaee866f 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -43,46 +43,6 @@ _LOGGER = logging.getLogger(__name__) -SENSOR_OUTSIDE_TEMPERATURE = "outside_temperature" -SENSOR_SUPPLY_TEMPERATURE = "supply_temperature" -SENSOR_RETURN_TEMPERATURE = "return_temperature" - -# gas sensors -SENSOR_BOILER_TEMPERATURE = "boiler_temperature" -SENSOR_BURNER_MODULATION = "burner_modulation" -SENSOR_BURNER_STARTS = "burner_starts" -SENSOR_BURNER_HOURS = "burner_hours" -SENSOR_BURNER_POWER = "burner_power" -SENSOR_DHW_GAS_CONSUMPTION_TODAY = "hotwater_gas_consumption_today" -SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK = "hotwater_gas_consumption_heating_this_week" -SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH = "hotwater_gas_consumption_heating_this_month" -SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR = "hotwater_gas_consumption_heating_this_year" -SENSOR_GAS_CONSUMPTION_TODAY = "gas_consumption_heating_today" -SENSOR_GAS_CONSUMPTION_THIS_WEEK = "gas_consumption_heating_this_week" -SENSOR_GAS_CONSUMPTION_THIS_MONTH = "gas_consumption_heating_this_month" -SENSOR_GAS_CONSUMPTION_THIS_YEAR = "gas_consumption_heating_this_year" - -# heatpump sensors -SENSOR_COMPRESSOR_STARTS = "compressor_starts" -SENSOR_COMPRESSOR_HOURS = "compressor_hours" -SENSOR_COMPRESSOR_HOURS_LOADCLASS1 = "compressor_hours_loadclass1" -SENSOR_COMPRESSOR_HOURS_LOADCLASS2 = "compressor_hours_loadclass2" -SENSOR_COMPRESSOR_HOURS_LOADCLASS3 = "compressor_hours_loadclass3" -SENSOR_COMPRESSOR_HOURS_LOADCLASS4 = "compressor_hours_loadclass4" -SENSOR_COMPRESSOR_HOURS_LOADCLASS5 = "compressor_hours_loadclass5" - -# fuelcell sensors -SENSOR_POWER_PRODUCTION_CURRENT = "power_production_current" -SENSOR_POWER_PRODUCTION_TODAY = "power_production_today" -SENSOR_POWER_PRODUCTION_THIS_WEEK = "power_production_this_week" -SENSOR_POWER_PRODUCTION_THIS_MONTH = "power_production_this_month" -SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" - -# solar sensors -SENSOR_COLLECTOR_TEMPERATURE = "collector temperature" -SENSOR_SOLAR_STORAGE_TEMPERATURE = "solar storage temperature" -SENSOR_SOLAR_POWER_PRODUCTION = "solar power production" - @dataclass class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin): @@ -93,84 +53,87 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( - key=SENSOR_OUTSIDE_TEMPERATURE, + key="outside_temperature", name="Outside Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getOutsideTemperature(), device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( - key=SENSOR_RETURN_TEMPERATURE, + key="return_temperature", name="Return Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getReturnTemperature(), device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( - key=SENSOR_BOILER_TEMPERATURE, + key="boiler_temperature", name="Boiler Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getBoilerTemperature(), device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( - key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, + key="hotwater_gas_consumption_today", name="Hot water gas consumption today", value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, + key="hotwater_gas_consumption_heating_this_week", name="Hot water gas consumption this week", value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, + key="hotwater_gas_consumption_heating_this_month", name="Hot water gas consumption this month", value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, + key="hotwater_gas_consumption_heating_this_year", name="Hot water gas consumption this year", value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), unit_getter=lambda api: api.getGasConsumptionDomesticHotWaterUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_GAS_CONSUMPTION_TODAY, + key="gas_consumption_heating_today", name="Heating gas consumption today", value_getter=lambda api: api.getGasConsumptionHeatingToday(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, + key="gas_consumption_heating_this_week", name="Heating gas consumption this week", value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, + key="gas_consumption_heating_this_month", name="Heating gas consumption this month", value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, + key="gas_consumption_heating_this_year", name="Heating gas consumption this year", value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_POWER_PRODUCTION_CURRENT, + key="power_production_current", name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), @@ -178,7 +141,7 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( - key=SENSOR_POWER_PRODUCTION_TODAY, + key="power_production_today", name="Power production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionToday(), @@ -186,7 +149,7 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_POWER_PRODUCTION_THIS_WEEK, + key="power_production_this_week", name="Power production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisWeek(), @@ -194,7 +157,7 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_POWER_PRODUCTION_THIS_MONTH, + key="power_production_this_month", name="Power production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisMonth(), @@ -202,7 +165,7 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_POWER_PRODUCTION_THIS_YEAR, + key="power_production_this_year", name="Power production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisYear(), @@ -210,24 +173,90 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_SOLAR_STORAGE_TEMPERATURE, + key="solar storage temperature", name="Solar Storage Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getSolarStorageTemperature(), device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ViCareSensorEntityDescription( - key=SENSOR_COLLECTOR_TEMPERATURE, + key="collector temperature", name="Solar Collector Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getSolarCollectorTemperature(), device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="solar power production today", + name="Solar power production today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getSolarPowerProductionToday(), + unit_getter=lambda api: api.getSolarPowerProductionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_SOLAR_POWER_PRODUCTION, - name="Solar Power Production", + key="solar power production this week", + name="Solar power production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getSolarPowerProduction(), + value_getter=lambda api: api.getSolarPowerProductionThisWeek(), + unit_getter=lambda api: api.getSolarPowerProductionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="solar power production this month", + name="Solar power production this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getSolarPowerProductionThisMonth(), + unit_getter=lambda api: api.getSolarPowerProductionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="solar power production this year", + name="Solar power production this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getSolarPowerProductionThisYear(), + unit_getter=lambda api: api.getSolarPowerProductionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="power consumption today", + name="Power consumption today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerConsumptionToday(), + unit_getter=lambda api: api.getPowerConsumptionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="power consumption this week", + name="Power consumption this week", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerConsumptionThisWeek(), + unit_getter=lambda api: api.getPowerConsumptionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="power consumption this month", + name="Power consumption this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerConsumptionThisMonth(), + unit_getter=lambda api: api.getPowerConsumptionUnit(), + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="power consumption this year", + name="Power consumption this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerConsumptionThisYear(), + unit_getter=lambda api: api.getPowerConsumptionUnit(), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -235,84 +264,96 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( - key=SENSOR_SUPPLY_TEMPERATURE, + key="supply_temperature", name="Supply Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getSupplyTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ) BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( - key=SENSOR_BURNER_STARTS, + key="burner_starts", name="Burner Starts", icon="mdi:counter", value_getter=lambda api: api.getStarts(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_BURNER_HOURS, + key="burner_hours", name="Burner Hours", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHours(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_BURNER_MODULATION, + key="burner_modulation", name="Burner Modulation", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, value_getter=lambda api: api.getModulation(), + state_class=SensorStateClass.MEASUREMENT, ), ) COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_STARTS, + key="compressor_starts", name="Compressor Starts", icon="mdi:counter", value_getter=lambda api: api.getStarts(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS, + key="compressor_hours", name="Compressor Hours", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHours(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1, + key="compressor_hours_loadclass1", name="Compressor Hours Load Class 1", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHoursLoadClass1(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS2, + key="compressor_hours_loadclass2", name="Compressor Hours Load Class 2", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHoursLoadClass2(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS3, + key="compressor_hours_loadclass3", name="Compressor Hours Load Class 3", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHoursLoadClass3(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS4, + key="compressor_hours_loadclass4", name="Compressor Hours Load Class 4", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHoursLoadClass4(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ViCareSensorEntityDescription( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS5, + key="compressor_hours_loadclass5", name="Compressor Hours Load Class 5", icon="mdi:counter", native_unit_of_measurement=TIME_HOURS, value_getter=lambda api: api.getHoursLoadClass5(), + state_class=SensorStateClass.TOTAL_INCREASING, ), ) @@ -426,6 +467,7 @@ def device_info(self): "name": self._device_config.getModel(), "manufacturer": "Viessmann", "model": (DOMAIN, self._device_config.getModel()), + "configuration_url": "https://developer.viessmann.com/", } @property diff --git a/homeassistant/components/vicare/translations/el.json b/homeassistant/components/vicare/translations/el.json index 1109812260d53..4c4385ec88b81 100644 --- a/homeassistant/components/vicare/translations/el.json +++ b/homeassistant/components/vicare/translations/el.json @@ -1,12 +1,25 @@ { "config": { + "abort": { + "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": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "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" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "client_id": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "heating_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", - "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)" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "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)", + "username": "Email" }, - "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 ViCare. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.viessmann.com" + "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 ViCare. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.viessmann.com", + "title": "{name}" } } } diff --git a/homeassistant/components/vicare/translations/pt-BR.json b/homeassistant/components/vicare/translations/pt-BR.json new file mode 100644 index 0000000000000..01504272dacc6 --- /dev/null +++ b/homeassistant/components/vicare/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "{name} ( {host} )", + "step": { + "user": { + "data": { + "client_id": "Chave da API", + "heating_type": "Tipo de aquecimento", + "name": "Nome", + "password": "Senha", + "scan_interval": "Intervalo de varredura (segundos)", + "username": "Email" + }, + "description": "Configure a integra\u00e7\u00e3o do ViCare. Para gerar a chave de API, acesse https://developer.viessmann.com", + "title": "{name}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/sk.json b/homeassistant/components/vicare/translations/sk.json new file mode 100644 index 0000000000000..d68d88fd2cdae --- /dev/null +++ b/homeassistant/components/vicare/translations/sk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "client_id": "API k\u013e\u00fa\u010d", + "name": "N\u00e1zov", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json index 648acb7e35fc7..c86ebf3cb342f 100644 --- a/homeassistant/components/vicare/translations/zh-Hant.json +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 0107ff8fe4ced..139c8f75e7f93 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -56,18 +56,6 @@ } -def _build_entity(name, vicare_api, circuit, device_config, heating_type): - """Create a ViCare water_heater entity.""" - _LOGGER.debug("Found device %s", name) - return ViCareWater( - name, - vicare_api, - circuit, - device_config, - heating_type, - ) - - def _get_circuits(vicare_api): """Return the list of circuits.""" try: @@ -93,7 +81,7 @@ async def async_setup_entry( if len(circuits) > 1: suffix = f" {circuit.id}" - entity = _build_entity( + entity = ViCareWater( f"{name} Water{suffix}", api, circuit, @@ -159,6 +147,7 @@ def device_info(self): "name": self._device_config.getModel(), "manufacturer": "Viessmann", "model": (DOMAIN, self._device_config.getModel()), + "configuration_url": "https://developer.viessmann.com/", } @property diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json index 568db1afdc04a..e14dc58cf294b 100644 --- a/homeassistant/components/vilfo/manifest.json +++ b/homeassistant/components/vilfo/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/vilfo", "requirements": ["vilfo-api-client==0.3.2"], "codeowners": ["@ManneW"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["vilfo"] } diff --git a/homeassistant/components/vilfo/translations/el.json b/homeassistant/components/vilfo/translations/el.json new file mode 100644 index 0000000000000..7af0b69e97e82 --- /dev/null +++ b/homeassistant/components/vilfo/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", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "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 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Vilfo. \u03a7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03c4\u03bf hostname/IP \u03c4\u03bf\u03c5 Vilfo Router \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 API. \u0393\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 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2, \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5: https://www.home-assistant.io/integrations/vilfo", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/pt-BR.json b/homeassistant/components/vilfo/translations/pt-BR.json index 3105455cb8baa..8766261955f80 100644 --- a/homeassistant/components/vilfo/translations/pt-BR.json +++ b/homeassistant/components/vilfo/translations/pt-BR.json @@ -1,15 +1,20 @@ { "config": { "abort": { - "already_configured": "Este roteador Vilfo j\u00e1 est\u00e1 configurado." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar. Por favor, verifique as informa\u00e7\u00f5es fornecidas por voc\u00ea e tente novamente.", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida. Verifique o token de acesso e tente novamente.", - "unknown": "Ocorreu um erro inesperado ao configurar a integra\u00e7\u00e3o." + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "user": { + "data": { + "access_token": "Token de acesso", + "host": "Nome do host" + }, + "description": "Configure a integra\u00e7\u00e3o do roteador Vilfo. Voc\u00ea precisa do seu nome de host/IP do roteador Vilfo e um token de acesso \u00e0 API. Para obter informa\u00e7\u00f5es adicionais sobre essa integra\u00e7\u00e3o e como obter esses detalhes, visite: https://www.home-assistant.io/integrations/vilfo", "title": "Conecte-se ao roteador Vilfo" } } diff --git a/homeassistant/components/vilfo/translations/sk.json b/homeassistant/components/vilfo/translations/sk.json new file mode 100644 index 0000000000000..7afa1eaea6e5d --- /dev/null +++ b/homeassistant/components/vilfo/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index c3a48b304021d..ba44a69478df9 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": ["libpyvivotek==0.4.0"], "codeowners": ["@HarlemSquirrel"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["libpyvivotek"] } diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index e66a8f3a55422..1e0d5a322fb69 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -9,7 +9,7 @@ from pyvizio.util import gen_apps_list_from_url import voluptuous as vol -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -24,13 +24,13 @@ def validate_apps(config: ConfigType) -> ConfigType: - """Validate CONF_APPS is only used when CONF_DEVICE_CLASS == DEVICE_CLASS_TV.""" + """Validate CONF_APPS is only used when CONF_DEVICE_CLASS is MediaPlayerDeviceClass.TV.""" if ( config.get(CONF_APPS) is not None - and config[CONF_DEVICE_CLASS] != DEVICE_CLASS_TV + and config[CONF_DEVICE_CLASS] != MediaPlayerDeviceClass.TV ): raise vol.Invalid( - f"'{CONF_APPS}' can only be used if {CONF_DEVICE_CLASS}' is '{DEVICE_CLASS_TV}'" + f"'{CONF_APPS}' can only be used if {CONF_DEVICE_CLASS}' is '{MediaPlayerDeviceClass.TV}'" ) return config @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) if ( CONF_APPS not in hass.data[DOMAIN] - and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV ): coordinator = VizioAppsDataUpdateCoordinator(hass) await coordinator.async_refresh() @@ -83,7 +83,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if not any( entry.state is ConfigEntryState.LOADED and entry.entry_id != config_entry.entry_id - and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV for entry in hass.config_entries.async_entries(DOMAIN) ): hass.data[DOMAIN].pop(CONF_APPS, None) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 9cca89f77aa74..019d016eb2a54 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.config_entries import ( SOURCE_IGNORE, SOURCE_IMPORT, @@ -68,7 +68,11 @@ def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: vol.Required( CONF_DEVICE_CLASS, default=input_dict.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), - ): vol.All(str, vol.Lower, vol.In([DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER])), + ): vol.All( + str, + vol.Lower, + vol.In([MediaPlayerDeviceClass.TV, MediaPlayerDeviceClass.SPEAKER]), + ), vol.Optional( CONF_ACCESS_TOKEN, default=input_dict.get(CONF_ACCESS_TOKEN, "") ): str, @@ -134,7 +138,7 @@ async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult } ) - if self.config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV: + if self.config_entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV: default_include_or_exclude = ( CONF_EXCLUDE if self.config_entry.options @@ -233,7 +237,9 @@ async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult self._must_show_form = False elif user_input[ CONF_DEVICE_CLASS - ] == DEVICE_CLASS_SPEAKER or user_input.get(CONF_ACCESS_TOKEN): + ] == MediaPlayerDeviceClass.SPEAKER or user_input.get( + CONF_ACCESS_TOKEN + ): # Ensure config is valid for a device if not await VizioAsync.validate_ha_config( user_input[CONF_HOST], diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index f686a6ac1fcee..5b534f861cc37 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], "quality_scale": "platinum", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyvizio"] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 664a8ae7da86e..a076a995f7bd0 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -10,9 +10,8 @@ from pyvizio.const import APP_HOME, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP from homeassistant.components.media_player import ( - DEVICE_CLASS_SPEAKER, - DEVICE_CLASS_TV, SUPPORT_SELECT_SOUND_MODE, + MediaPlayerDeviceClass, MediaPlayerEntity, ) from homeassistant.config_entries import ConfigEntry @@ -252,7 +251,7 @@ async def async_update(self) -> None: self._available_inputs = [input_.name for input_ in inputs] # Return before setting app variables if INPUT_APPS isn't in available inputs - if self._attr_device_class == DEVICE_CLASS_SPEAKER or not any( + if self._attr_device_class == MediaPlayerDeviceClass.SPEAKER or not any( app for app in INPUT_APPS if app in self._available_inputs ): return @@ -329,7 +328,7 @@ def apps_list_update(): self._all_apps = self._apps_coordinator.data self.async_write_ha_state() - if self._attr_device_class == DEVICE_CLASS_TV: + if self._attr_device_class == MediaPlayerDeviceClass.TV: self.async_on_remove( self._apps_coordinator.async_add_listener(apps_list_update) ) diff --git a/homeassistant/components/vizio/translations/cs.json b/homeassistant/components/vizio/translations/cs.json index cf0c66749ddb3..f2b8c74c25ab2 100644 --- a/homeassistant/components/vizio/translations/cs.json +++ b/homeassistant/components/vizio/translations/cs.json @@ -32,7 +32,7 @@ "host": "Hostitel", "name": "Jm\u00e9no" }, - "description": "P\u0159\u00edstupov\u00fd token je pot\u0159eba pouze pro televizory. Pokud konfigurujete televizor a nem\u00e1te [%key:common:common::config_flow::data::access_token%], ponechte jej pr\u00e1zdn\u00e9, abyste mohli proj\u00edt procesem p\u00e1rov\u00e1n\u00ed.", + "description": "P\u0159\u00edstupov\u00fd token je pot\u0159eba pouze pro televizory. Pokud konfigurujete televizor a nem\u00e1te P\u0159\u00edstupov\u00fd token, ponechte jej pr\u00e1zdn\u00e9, abyste mohli proj\u00edt procesem p\u00e1rov\u00e1n\u00ed.", "title": "Za\u0159\u00edzen\u00ed VIZIO SmartCast" } } diff --git a/homeassistant/components/vizio/translations/el.json b/homeassistant/components/vizio/translations/el.json index dbc2c9b5ed35e..128d16d9c4cae 100644 --- a/homeassistant/components/vizio/translations/el.json +++ b/homeassistant/components/vizio/translations/el.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "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", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "updated_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b1\u03bb\u03bb\u03ac \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1, \u03bf\u03b9 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03ae/\u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03bf\u03c0\u03cc\u03c4\u03b5 \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03b5\u03af \u03b1\u03bd\u03b1\u03bb\u03cc\u03b3\u03c9\u03c2." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "complete_pairing_failed": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf PIN \u03c0\u03bf\u03c5 \u03b4\u03ce\u03c3\u03b1\u03c4\u03b5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03cc\u03c2 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03b5\u03be\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03bd\u03b1 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c1\u03b5\u03cd\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c0\u03c1\u03b9\u03bd \u03c4\u03b7\u03bd \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae.", + "existing_config_entry_found": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 VIZIO SmartCast \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd." + }, "step": { "pair_tv": { - "description": "\u0397 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b7 \u03c6\u03cc\u03c1\u03bc\u03b1 \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03b2\u03ae\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7." + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "description": "\u0397 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b7 \u03c6\u03cc\u03c1\u03bc\u03b1 \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03b2\u03ae\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7.", + "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2" + }, + "pairing_complete": { + "description": "\u0397 VIZIO SmartCast \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03bf Home Assistant.", + "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" + }, + "pairing_complete_import": { + "description": "\u03a4\u03bf VIZIO SmartCast \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03c3\u03c4\u03bf Home Assistant. \n\n \u03a4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"**{access_token}**\".", + "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" }, "user": { "data": { - "device_class": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - } + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "device_class": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "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" + }, + "description": "\u0388\u03bd\u03b1 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03c4\u03b7\u03bb\u03b5\u03bf\u03c1\u03ac\u03c3\u03b5\u03b9\u03c2. \u0395\u03ac\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03b6\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ad\u03bd\u03b1 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b5\u03c1\u03ac\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c0\u03cc \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2.", + "title": "VIZIO SmartCast \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "\u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03af\u03bb\u03b7\u03c8\u03b7 \u03ae \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7", + "include_or_exclude": "\u03a3\u03c5\u03bc\u03c0\u03b5\u03c1\u03af\u03bb\u03b7\u03c8\u03b7 \u03ae \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd;", + "volume_step": "\u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ad\u03bd\u03c4\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 Smart TV, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ac \u03bd\u03b1 \u03c6\u03b9\u03bb\u03c4\u03c1\u03ac\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c0\u03b7\u03b3\u03ce\u03bd \u03c3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c0\u03bf\u03b9\u03b5\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ae \u03b8\u03b1 \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c0\u03b7\u03b3\u03ce\u03bd \u03c3\u03b1\u03c2.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 VIZIO SmartCast \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd" } } } diff --git a/homeassistant/components/vizio/translations/ja.json b/homeassistant/components/vizio/translations/ja.json index b40d9ef8680c6..ed2e934cbec94 100644 --- a/homeassistant/components/vizio/translations/ja.json +++ b/homeassistant/components/vizio/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "updated_entry": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u8a2d\u5b9a\u3067\u5b9a\u7fa9\u3055\u308c\u305f\u540d\u524d\u3001\u30a2\u30d7\u30ea\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u4ee5\u524d\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u305f\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u306f\u305d\u308c\u306b\u5fdc\u3058\u3066\u66f4\u65b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + "updated_entry": "\u3053\u306e\u30a8\u30f3\u30c8\u30ea\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u8a2d\u5b9a\u3067\u5b9a\u7fa9\u3055\u308c\u305f\u540d\u524d\u3001\u30a2\u30d7\u30ea\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u4ee5\u524d\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u305f\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u306a\u304b\u3063\u305f\u305f\u3081\u3001\u8a2d\u5b9a\u30a8\u30f3\u30c8\u30ea\u306f\u3059\u3067\u306b\u305d\u308c\u306b\u5fdc\u3058\u3066\u66f4\u65b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/vizio/translations/pt-BR.json b/homeassistant/components/vizio/translations/pt-BR.json index bca1aeeaf3d91..785bbd752947a 100644 --- a/homeassistant/components/vizio/translations/pt-BR.json +++ b/homeassistant/components/vizio/translations/pt-BR.json @@ -1,24 +1,53 @@ { "config": { + "abort": { + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "updated_entry": "Esta entrada j\u00e1 foi configurada, mas o nome, aplicativos e/ou op\u00e7\u00f5es definidas na configura\u00e7\u00e3o n\u00e3o correspondem \u00e0 configura\u00e7\u00e3o anteriormente importada, portanto a entrada de configura\u00e7\u00e3o foi atualizada de acordo." + }, "error": { + "cannot_connect": "Falha ao conectar", "complete_pairing_failed": "N\u00e3o foi poss\u00edvel concluir o pareamento. Verifique se o PIN que voc\u00ea forneceu est\u00e1 correto e a TV ainda est\u00e1 ligada e conectada \u00e0 internet antes de reenviar.", - "existing_config_entry_found": "Uma entrada j\u00e1 existente configurada com o mesmo n\u00famero de s\u00e9rie j\u00e1 foi configurada. Voc\u00ea deve apagar a entrada existente para poder configurar esta." + "existing_config_entry_found": "Uma entrada j\u00e1 existente Dispositivo VIZIO SmartCast configurada com o mesmo n\u00famero de s\u00e9rie j\u00e1 foi configurada. Voc\u00ea deve apagar a entrada existente para poder configurar esta." }, "step": { "pair_tv": { "data": { - "pin": "PIN" + "pin": "C\u00f3digo PIN" }, "description": "Sua TV deve estar exibindo um c\u00f3digo. Digite esse c\u00f3digo no formul\u00e1rio e continue na pr\u00f3xima etapa para concluir o pareamento.", "title": "Processo de pareamento completo" }, "pairing_complete": { + "description": "Seu Dispositivo VIZIO SmartCast agora est\u00e1 conectado ao Home Assistant.", "title": "Pareamento completo" }, + "pairing_complete_import": { + "description": "Seu Dispositivo VIZIO SmartCast agora est\u00e1 conectado ao Home Assistant.\n\nSeu Token de acesso \u00e9 '**{access_token}**'.", + "title": "Emparelhamento conclu\u00eddo" + }, "user": { "data": { - "device_class": "Tipo de dispositivo" - } + "access_token": "Token de acesso", + "device_class": "Tipo de dispositivo", + "host": "Nome do host", + "name": "Nome" + }, + "description": "Um Token de acesso s\u00f3 \u00e9 necess\u00e1rio para TVs. Se voc\u00ea estiver configurando uma TV e ainda n\u00e3o tiver um Token de acesso , deixe-o em branco para passar pelo processo de pareamento.", + "title": "Dispositivo VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "Aplicativos para incluir ou excluir", + "include_or_exclude": "Incluir ou excluir aplicativos?", + "volume_step": "Tamanho do Passo do Volume" + }, + "description": "Se voc\u00ea tiver uma Smart TV, poder\u00e1 filtrar sua lista de fontes opcionalmente escolhendo quais aplicativos incluir ou excluir em sua lista de fontes.", + "title": "Alterar op\u00e7\u00f5es para Dispositivo VIZIO SmartCast" } } } diff --git a/homeassistant/components/vizio/translations/sk.json b/homeassistant/components/vizio/translations/sk.json new file mode 100644 index 0000000000000..171a5a2c70896 --- /dev/null +++ b/homeassistant/components/vizio/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Pr\u00edstupov\u00fd token", + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index aa3721fe6452c..2d2b01cb04b5f 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vlc_telnet", "requirements": ["aiovlc==0.1.0"], - "codeowners": ["@rodripf", "@dmcc", "@MartinHjelmare"], - "iot_class": "local_polling" + "codeowners": ["@rodripf", "@MartinHjelmare"], + "iot_class": "local_polling", + "loggers": ["aiovlc"] } diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 11930aada99c2..140c2b2c253a9 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -2,18 +2,20 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine -from datetime import datetime, timedelta +from datetime import datetime from functools import wraps from typing import Any, TypeVar -from urllib.parse import quote from aiovlc.client import Client from aiovlc.exceptions import AuthError, CommandError, ConnectError from typing_extensions import Concatenate, ParamSpec from homeassistant.components import media_source -from homeassistant.components.http.auth import async_sign_path -from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity +from homeassistant.components.media_player import ( + BrowseMedia, + MediaPlayerEntity, + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_BROWSE_MEDIA, @@ -32,10 +34,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.network import get_url import homeassistant.util.dt as dt_util from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DOMAIN, LOGGER @@ -305,28 +307,16 @@ async def async_play_media( # Handle media_source if media_source.is_media_source_id(media_id): sourced_media = await media_source.async_resolve_media(self.hass, media_id) - media_type = MEDIA_TYPE_MUSIC + media_type = sourced_media.mime_type media_id = sourced_media.url - # Sign and prefix with URL if playing a relative URL - if media_id[0] == "/": - media_id = async_sign_path( - self.hass, - quote(media_id), - timedelta(seconds=media_source.DEFAULT_EXPIRY_TIME), + if media_type != MEDIA_TYPE_MUSIC and not media_type.startswith("audio/"): + raise HomeAssistantError( + f"Invalid media type {media_type}. Only {MEDIA_TYPE_MUSIC} is supported" ) - # prepend external URL - hass_url = get_url(self.hass) - media_id = f"{hass_url}{media_id}" - - if media_type != MEDIA_TYPE_MUSIC: - LOGGER.error( - "Invalid media type %s. Only %s is supported", - media_type, - MEDIA_TYPE_MUSIC, - ) - return + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) await self._vlc.add(media_id) self._state = STATE_PLAYING diff --git a/homeassistant/components/vlc_telnet/translations/el.json b/homeassistant/components/vlc_telnet/translations/el.json new file mode 100644 index 0000000000000..3b1aac109e015 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/el.json @@ -0,0 +1,36 @@ +{ + "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", + "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", + "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", + "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" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf {addon};" + }, + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03c9\u03c3\u03c4\u03cc \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae: {host}" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pt-BR.json b/homeassistant/components/vlc_telnet/translations/pt-BR.json index f9028aae0023e..0a26a6aaf7d75 100644 --- a/homeassistant/components/vlc_telnet/translations/pt-BR.json +++ b/homeassistant/components/vlc_telnet/translations/pt-BR.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Voc\u00ea quer se conectar para adicionar {addon}?" + }, "reauth_confirm": { "data": { "password": "Senha" - } + }, + "description": "Digite a senha correta para o host: {host}" }, "user": { "data": { + "host": "Nome do host", "name": "Nome", "password": "Senha", "port": "Porta" diff --git a/homeassistant/components/vlc_telnet/translations/sk.json b/homeassistant/components/vlc_telnet/translations/sk.json new file mode 100644 index 0000000000000..d3bc93c4168ab --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 11624da7f5329..286e18b0b17cb 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": ["volkszaehler==0.2.1"], "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["volkszaehler"] } diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index 818df8c83d940..3785ed0ecc742 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -6,5 +6,6 @@ "config_flow": true, "zeroconf": ["_Volumio._tcp.local."], "requirements": ["pyvolumio==0.1.5"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyvolumio"] } diff --git a/homeassistant/components/volumio/translations/el.json b/homeassistant/components/volumio/translations/el.json index c0cd55577314b..4a2d8b6f54f35 100644 --- a/homeassistant/components/volumio/translations/el.json +++ b/homeassistant/components/volumio/translations/el.json @@ -1,12 +1,23 @@ { "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": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Volumio \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5" }, + "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": { "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Volumio (`{name}`) \u03c3\u03c4\u03bf Home Assistant;", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf Volumio" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } } } } diff --git a/homeassistant/components/volumio/translations/pt-BR.json b/homeassistant/components/volumio/translations/pt-BR.json new file mode 100644 index 0000000000000..487710baf01a6 --- /dev/null +++ b/homeassistant/components/volumio/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "N\u00e3o foi poss\u00edvel conectar o Audi\u00f3filo descoberto" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "discovery_confirm": { + "description": "Voc\u00ea quer adicionar o audi\u00f3filo {name} ao Home Assistant?", + "title": "O dispositivo foi encontrado" + }, + "user": { + "data": { + "host": "Nome do host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ru.json b/homeassistant/components/volumio/translations/ru.json index 905ecbe8e4b69..9ce0080b21850 100644 --- a/homeassistant/components/volumio/translations/ru.json +++ b/homeassistant/components/volumio/translations/ru.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Volumio `{name}`?", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 Volumio" + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d Volumio" }, "user": { "data": { diff --git a/homeassistant/components/volumio/translations/sk.json b/homeassistant/components/volumio/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/volumio/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index eac179efa8d80..48caa75a82496 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.9.1"], "codeowners": ["@molobrakos", "@decompil3d"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["geopy", "hbmqtt", "volvooncall"] } diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 0fbd4e2ebe4da..449b9a33e34d3 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/vultr", "requirements": ["vultr==0.1.2"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["vultr"] } diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index 6089c00be489a..1a754351e7b9b 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/w800rf32", "requirements": ["pyW800rf32==0.1"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["W800rf32"] } diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index aeadf541345a9..2a4978b1cc104 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -9,5 +9,6 @@ "homekit": {}, "dependencies": [], "codeowners": ["@hesselonline"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["wallbox"] } diff --git a/homeassistant/components/wallbox/translations/cs.json b/homeassistant/components/wallbox/translations/cs.json new file mode 100644 index 0000000000000..72df4a968182f --- /dev/null +++ b/homeassistant/components/wallbox/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/el.json b/homeassistant/components/wallbox/translations/el.json index da02cbb297fb4..dd95266866f80 100644 --- a/homeassistant/components/wallbox/translations/el.json +++ b/homeassistant/components/wallbox/translations/el.json @@ -1,12 +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", + "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": { - "reauth_invalid": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u039f \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc" + "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", + "reauth_invalid": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u039f \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "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" + } + }, "user": { "data": { - "station": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd" + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "station": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } } diff --git a/homeassistant/components/wallbox/translations/nb.json b/homeassistant/components/wallbox/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/wallbox/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt-BR.json b/homeassistant/components/wallbox/translations/pt-BR.json new file mode 100644 index 0000000000000..3fb6428603a58 --- /dev/null +++ b/homeassistant/components/wallbox/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_invalid": "Falha na reautentica\u00e7\u00e3o; N\u00famero de s\u00e9rie n\u00e3o corresponde ao original", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "user": { + "data": { + "password": "Senha", + "station": "N\u00famero de s\u00e9rie da esta\u00e7\u00e3o", + "username": "Usu\u00e1rio" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/sk.json b/homeassistant/components/wallbox/translations/sk.json new file mode 100644 index 0000000000000..71a7aea5018f3 --- /dev/null +++ b/homeassistant/components/wallbox/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 48f812f447a9e..d4818d4462686 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "requirements": ["waqiasync==1.0.0"], "codeowners": ["@andrey-git"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["waqiasync"] } diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 8a3b9f046ce30..499233717b604 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -91,15 +91,11 @@ async def async_setup_platform( _LOGGER.debug("The following stations were returned: %s", stations) for station in stations: waqi_sensor = WaqiSensor(client, station) - if ( - not station_filter - or { - waqi_sensor.uid, - waqi_sensor.url, - waqi_sensor.station_name, - } - & set(station_filter) - ): + if not station_filter or { + waqi_sensor.uid, + waqi_sensor.url, + waqi_sensor.station_name, + } & set(station_filter): dev.append(waqi_sensor) except ( aiohttp.client_exceptions.ClientConnectorError, diff --git a/homeassistant/components/water_heater/translations/pt-BR.json b/homeassistant/components/water_heater/translations/pt-BR.json new file mode 100644 index 0000000000000..28e234b4d74d7 --- /dev/null +++ b/homeassistant/components/water_heater/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "El\u00e9trico", + "gas": "G\u00e1s", + "heat_pump": "Bomba de calor", + "high_demand": "Alta demanda", + "off": "Desligado", + "performance": "Desempenho" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 82f60abbd64e9..8699df289d74f 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "requirements": ["waterfurnace==1.1.0"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["waterfurnace"] } diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index 95f5b3c7d0a97..7b65b5d0faa11 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/watson_iot", "requirements": ["ibmiotf==0.3.4"], "codeowners": [], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["ibmiotf", "paho_mqtt"] } diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index cf70a8088293e..f225ac25ae72b 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": ["ibm-watson==5.2.2"], "codeowners": ["@rutkai"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["ibm_cloud_sdk_core", "ibm_watson"] } diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 415697670b0eb..993e070ffe88b 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -18,7 +18,6 @@ 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.typing import ConfigType from .const import ( CONF_BALANCING_AUTHORITY, @@ -190,7 +189,7 @@ async def async_step_location( ) return await self.async_step_coordinates() - async def async_step_reauth(self, config: ConfigType) -> FlowResult: + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._data = {**config} return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/watttime/manifest.json b/homeassistant/components/watttime/manifest.json index 85a32bce3314a..95c5362406925 100644 --- a/homeassistant/components/watttime/manifest.json +++ b/homeassistant/components/watttime/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@bachya" ], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["aiowatttime"] } diff --git a/homeassistant/components/watttime/translations/el.json b/homeassistant/components/watttime/translations/el.json new file mode 100644 index 0000000000000..dd63707241325 --- /dev/null +++ b/homeassistant/components/watttime/translations/el.json @@ -0,0 +1,52 @@ +{ + "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", + "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_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", + "unknown_coordinates": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b3\u03b9\u03b1 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2/\u03bc\u03ae\u03ba\u03bf\u03c2" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03ae\u03ba\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5:" + }, + "location": { + "data": { + "location_type": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7:" + }, + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username}:", + "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": { + "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": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2:" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf\u03bd \u03c7\u03ac\u03c1\u03c4\u03b7" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 WattTime" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/nb.json b/homeassistant/components/watttime/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/watttime/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/pt-BR.json b/homeassistant/components/watttime/translations/pt-BR.json index a522da7febd29..286a714eac499 100644 --- a/homeassistant/components/watttime/translations/pt-BR.json +++ b/homeassistant/components/watttime/translations/pt-BR.json @@ -1,21 +1,51 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado", + "unknown_coordinates": "Sem dados para latitude/longitude" + }, "step": { + "coordinates": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Insira a latitude e longitude para monitorar:" + }, "location": { "data": { "location_type": "Localiza\u00e7\u00e3o" - } + }, + "description": "Escolha um local para monitorar:" }, "reauth_confirm": { "data": { "password": "Senha" - } + }, + "description": "Por favor, digite novamente a senha para {username} :", + "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "description": "Insira seu nome de usu\u00e1rio e senha:" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar localiza\u00e7\u00e3o monitorada no mapa" + }, + "title": "Configurar WattTime" } } } diff --git a/homeassistant/components/watttime/translations/sk.json b/homeassistant/components/watttime/translations/sk.json new file mode 100644 index 0000000000000..1d9ecbee3fccd --- /dev/null +++ b/homeassistant/components/watttime/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + }, + "location": { + "data": { + "location_type": "Umiestnenie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py index 554f3ecf6d8c6..37278543dfb6b 100644 --- a/homeassistant/components/waze_travel_time/const.py +++ b/homeassistant/components/waze_travel_time/const.py @@ -25,6 +25,3 @@ REGIONS = ["US", "NA", "EU", "IL", "AU"] VEHICLE_TYPES = ["car", "taxi", "motorcycle"] - -# Attempt to find entity_id without finding address with period. -ENTITY_ID_PATTERN = "(? None: """Handle when entity is added.""" @@ -177,16 +164,8 @@ async def first_update(self, _=None): def update(self): """Fetch new state data for the sensor.""" _LOGGER.debug("Fetching Route for %s", self._attr_name) - # Get origin latitude and longitude from entity_id. - if self._origin_entity_id is not None: - self._waze_data.origin = find_coordinates(self.hass, self._origin_entity_id) - - # Get destination latitude and longitude from entity_id. - if self._destination_entity_id is not None: - self._waze_data.destination = find_coordinates( - self.hass, self._destination_entity_id - ) - + self._waze_data.origin = find_coordinates(self.hass, self._origin) + self._waze_data.destination = find_coordinates(self.hass, self._destination) self._waze_data.update() @@ -205,6 +184,11 @@ def __init__(self, origin, destination, region, config_entry): def update(self): """Update WazeRouteCalculator Sensor.""" + _LOGGER.debug( + "Getting update for origin: %s destination: %s", + self.origin, + self.destination, + ) if self.origin is not None and self.destination is not None: # Grab options on every update incl_filter = self.config_entry.options.get(CONF_INCL_FILTER) @@ -255,7 +239,7 @@ def update(self): if units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. - self.distance = distance / 1.609 + self.distance = IMPERIAL_SYSTEM.length(distance, LENGTH_KILOMETERS) else: self.distance = distance diff --git a/homeassistant/components/waze_travel_time/translations/el.json b/homeassistant/components/waze_travel_time/translations/el.json index 2dbb86c6dd4c6..e9a64002372f5 100644 --- a/homeassistant/components/waze_travel_time/translations/el.json +++ b/homeassistant/components/waze_travel_time/translations/el.json @@ -1,10 +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" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "step": { "user": { "data": { - "destination": "\u03a0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2" - } + "destination": "\u03a0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "origin": "\u03a0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7", + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" + }, + "description": "\u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc, \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03ae \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 GPS \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 (\u03bf\u03b9 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 GPS \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1). \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03ae \u03c4\u03bf\u03c5, \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03bf\u03cd \u03c0\u03bb\u03ac\u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03b9 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03bf\u03cd \u03bc\u03ae\u03ba\u03bf\u03c5\u03c2 \u03ae \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c6\u03b9\u03bb\u03b9\u03ba\u03cc \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b7 \u03b6\u03ce\u03bd\u03b7." } } }, @@ -12,6 +22,12 @@ "step": { "init": { "data": { + "avoid_ferries": "\u0391\u03c0\u03bf\u03c6\u03cd\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c0\u03bb\u03bf\u03af\u03b1;", + "avoid_subscription_roads": "\u0391\u03c0\u03bf\u03c6\u03cd\u03b3\u03b5\u03c4\u03b5 \u03b4\u03c1\u03cc\u03bc\u03bf\u03c5\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b2\u03b9\u03bd\u03b9\u03ad\u03c4\u03b1 / \u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03ae;", + "avoid_toll_roads": "\u0391\u03c0\u03bf\u03c6\u03cd\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03cc\u03b4\u03b9\u03b1;", + "excl_filter": "\u03a5\u03c0\u03bf\u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u039f\u03a7\u0399 \u03c3\u03c4\u03b7\u03bd \u03a0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2", + "incl_filter": "\u03a5\u03c0\u03bf\u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03c4\u03b7\u03bd \u03a0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2", + "realtime": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03c4\u03b1\u03be\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf;", "units": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b5\u03c2", "vehicle_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" }, diff --git a/homeassistant/components/waze_travel_time/translations/pt-BR.json b/homeassistant/components/waze_travel_time/translations/pt-BR.json new file mode 100644 index 0000000000000..54ade45119bdc --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/pt-BR.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "destination": "Destino", + "name": "Nome", + "origin": "Origem", + "region": "Regi\u00e3o" + }, + "description": "Para Origem e Destino, insira o endere\u00e7o ou as coordenadas GPS do local (as coordenadas GPS devem ser separadas por uma v\u00edrgula). Voc\u00ea tamb\u00e9m pode inserir um ID de entidade que forne\u00e7a essas informa\u00e7\u00f5es em seu estado, um ID de entidade com atributos de latitude e longitude ou um nome amig\u00e1vel de zona." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "Evitar balsas?", + "avoid_subscription_roads": "Evitar estradas que precisam de uma vinheta/assinatura?", + "avoid_toll_roads": "Evitar estradas com ped\u00e1gio?", + "excl_filter": "SEM Substring na descri\u00e7\u00e3o da rota selecionada", + "incl_filter": "Substring na descri\u00e7\u00e3o da rota selecionada", + "realtime": "Tempo de viagem em tempo real?", + "units": "Unidades", + "vehicle_type": "Tipo de Ve\u00edculo" + }, + "description": "As entradas `substring` permitir\u00e3o que voc\u00ea force a integra\u00e7\u00e3o a usar uma rota espec\u00edfica ou evite uma rota espec\u00edfica em seu c\u00e1lculo de viagem no tempo." + } + } + }, + "title": "Tempo de viagem do Waze" +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/sk.json b/homeassistant/components/waze_travel_time/translations/sk.json new file mode 100644 index 0000000000000..ce32d575ee2e0 --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/pt-BR.json b/homeassistant/components/weather/translations/pt-BR.json index 64a81da9b354b..bf5f96d849176 100644 --- a/homeassistant/components/weather/translations/pt-BR.json +++ b/homeassistant/components/weather/translations/pt-BR.json @@ -7,15 +7,15 @@ "fog": "Nevoeiro", "hail": "Granizo", "lightning": "Raios", - "lightning-rainy": "Raios, chuvoso", + "lightning-rainy": "Chuvoso com raios", "partlycloudy": "Parcialmente nublado", "pouring": "Torrencial", "rainy": "Chuvoso", "snowy": "Neve", - "snowy-rainy": "Neve, chuva", + "snowy-rainy": "Chuvoso com neve", "sunny": "Ensolarado", - "windy": "Ventoso", - "windy-variant": "Ventoso" + "windy": "Ventania", + "windy-variant": "Ventania" } } } \ No newline at end of file diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 46fdc89871f26..95233cca9ca83 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -95,7 +95,7 @@ async def async_handle_webhook( else: received_from = request.remote - _LOGGER.warning( + _LOGGER.info( "Received message for unregistered webhook %s from %s", webhook_id, received_from, diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 4c8ff6e5fd374..18338e86f1ac5 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -20,7 +20,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType from . import async_control_connect from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS @@ -172,7 +171,9 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: self.host = config_entry.data[CONF_HOST] self.key = config_entry.data[CONF_CLIENT_SECRET] - async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 47cdf974cc76e..feb5bae98fe68 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -79,9 +79,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE | None: """Attach a trigger.""" - trigger_type = config[CONF_TYPE] - - if trigger_type == TURN_ON_PLATFORM_TYPE: + if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: trigger_config = { CONF_PLATFORM: trigger_type, CONF_DEVICE_ID: config[CONF_DEVICE_ID], diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index 70a253d5cebd8..0ee3805f42f31 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -20,9 +20,7 @@ def async_get_device_entry_by_device_id( Raises ValueError if device ID is invalid. """ device_reg = dr.async_get(hass) - device = device_reg.async_get(device_id) - - if device is None: + if (device := device_reg.async_get(device_id)) is None: raise ValueError(f"Device {device_id} is not a valid {DOMAIN} device.") return device diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 1494180dd0576..a60a12aba30b4 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,9 +3,10 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.1.2", "sqlalchemy==1.4.27"], + "requirements": ["aiowebostv==0.1.3", "sqlalchemy==1.4.27"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}], "quality_scale": "platinum", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["aiowebostv"] } \ No newline at end of file diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 5aac52f6f7bd8..67125c45ef5e0 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -34,6 +34,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, STATE_OFF, @@ -44,6 +45,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 . import WebOsClientWrapper from .const import ( @@ -121,9 +123,11 @@ async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: return cmd_wrapper -class LgWebOSMediaPlayerEntity(MediaPlayerEntity): +class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): """Representation of a LG webOS Smart TV.""" + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__( self, wrapper: WebOsClientWrapper, @@ -134,8 +138,9 @@ def __init__( """Initialize the webos device.""" self._wrapper = wrapper self._client: WebOsClient = wrapper.client - self._name = name - self._unique_id = unique_id + self._attr_assumed_state = True + self._attr_name = name + self._attr_unique_id = unique_id self._sources = sources # Assume that the TV is not paused @@ -144,8 +149,13 @@ def __init__( self._current_source = None self._source_list: dict = {} + self._supported_features: int = 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() + self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_signal_handler) ) @@ -154,6 +164,14 @@ async def async_added_to_hass(self) -> None: self.async_handle_state_update ) + if ( + self.state == STATE_OFF + and (state := await self.async_get_last_state()) is not None + ): + self._supported_features = ( + state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & ~SUPPORT_TURN_ON + ) + async def async_will_remove_from_hass(self) -> None: """Call disconnect on removal.""" self._client.unregister_state_update_callback(self.async_handle_state_update) @@ -173,10 +191,73 @@ async def async_signal_handler(self, data: dict[str, Any]) -> None: async def async_handle_state_update(self, _client: WebOsClient) -> None: """Update state from WebOsClient.""" - self.update_sources() + self._update_states() self.async_write_ha_state() - def update_sources(self) -> None: + def _update_states(self) -> None: + """Update entity state attributes.""" + self._update_sources() + + self._attr_state = STATE_ON if self._client.is_on else STATE_OFF + self._attr_is_volume_muted = cast(bool, self._client.muted) + + self._attr_volume_level = None + if self._client.volume is not None: + self._attr_volume_level = cast(float, self._client.volume / 100.0) + + self._attr_source = self._current_source + self._attr_source_list = sorted(self._source_list) + + self._attr_media_content_type = None + if self._client.current_app_id == LIVE_TV_APP_ID: + self._attr_media_content_type = MEDIA_TYPE_CHANNEL + + self._attr_media_title = None + if (self._client.current_app_id == LIVE_TV_APP_ID) and ( + self._client.current_channel is not None + ): + self._attr_media_title = cast( + str, self._client.current_channel.get("channelName") + ) + + self._attr_media_image_url = None + if self._client.current_app_id in self._client.apps: + icon: str = self._client.apps[self._client.current_app_id]["largeIcon"] + if not icon.startswith("http"): + icon = self._client.apps[self._client.current_app_id]["icon"] + self._attr_media_image_url = icon + + if self.state != STATE_OFF or not self._supported_features: + supported = SUPPORT_WEBOSTV + if self._client.sound_output in ("external_arc", "external_speaker"): + supported = supported | SUPPORT_WEBOSTV_VOLUME + elif self._client.sound_output != "lineout": + supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + + self._supported_features = supported + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, cast(str, self.unique_id))}, + manufacturer="LG", + name=self.name, + ) + + if self._client.system_info is not None or self.state != STATE_OFF: + maj_v = self._client.software_info.get("major_ver") + min_v = self._client.software_info.get("minor_ver") + if maj_v and min_v: + self._attr_device_info["sw_version"] = f"{maj_v}.{min_v}" + + if model := self._client.system_info.get("modelName"): + self._attr_device_info["model"] = model + + self._attr_extra_state_attributes = {} + if self._client.sound_output is not None or self.state != STATE_OFF: + self._attr_extra_state_attributes = { + ATTR_SOUND_OUTPUT: self._client.sound_output + } + + def _update_sources(self) -> None: """Update list of sources from current source, apps, inputs and configured list.""" source_list = self._source_list self._source_list = {} @@ -237,123 +318,13 @@ async def async_update(self) -> None: with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): await self._client.connect() - @property - def unique_id(self) -> str: - """Return the unique id of the device.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - - @property - def device_class(self) -> MediaPlayerDeviceClass: - """Return the device class of the device.""" - return MediaPlayerDeviceClass.TV - - @property - def state(self) -> str: - """Return the state of the device.""" - if self._client.is_on: - return STATE_ON - - return STATE_OFF - - @property - def is_volume_muted(self) -> bool: - """Boolean if volume is currently muted.""" - return cast(bool, self._client.muted) - - @property - def volume_level(self) -> float | None: - """Volume level of the media player (0..1).""" - if self._client.volume is not None: - return cast(float, self._client.volume / 100.0) - - return None - - @property - def source(self) -> str | None: - """Return the current input source.""" - return self._current_source - - @property - def source_list(self) -> list[str]: - """List of available input sources.""" - return sorted(self._source_list) - - @property - def media_content_type(self) -> str | None: - """Content type of current playing media.""" - if self._client.current_app_id == LIVE_TV_APP_ID: - return MEDIA_TYPE_CHANNEL - - return None - - @property - def media_title(self) -> str | None: - """Title of current playing media.""" - if (self._client.current_app_id == LIVE_TV_APP_ID) and ( - self._client.current_channel is not None - ): - return cast(str, self._client.current_channel.get("channelName")) - return None - - @property - def media_image_url(self) -> str | None: - """Image url of current playing media.""" - if self._client.current_app_id in self._client.apps: - icon: str = self._client.apps[self._client.current_app_id]["largeIcon"] - if not icon.startswith("http"): - icon = self._client.apps[self._client.current_app_id]["icon"] - return icon - return None - @property def supported_features(self) -> int: """Flag media player features that are supported.""" - supported = SUPPORT_WEBOSTV - - if self._client.sound_output in ("external_arc", "external_speaker"): - supported = supported | SUPPORT_WEBOSTV_VOLUME - elif self._client.sound_output != "lineout": - supported = supported | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET - if self._wrapper.turn_on: - supported |= SUPPORT_TURN_ON - - return supported + return self._supported_features | SUPPORT_TURN_ON - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - device_info = DeviceInfo( - identifiers={(DOMAIN, self._unique_id)}, - manufacturer="LG", - name=self._name, - ) - - if self._client.system_info is None and self.state == STATE_OFF: - return device_info - - maj_v = self._client.software_info.get("major_ver") - min_v = self._client.software_info.get("minor_ver") - if maj_v and min_v: - device_info["sw_version"] = f"{maj_v}.{min_v}" - - model = self._client.system_info.get("modelName") - if model: - device_info["model"] = model - - return device_info - - @property - def extra_state_attributes(self) -> dict[str, str] | None: - """Return device specific state attributes.""" - if self._client.sound_output is None and self.state == STATE_OFF: - return None - return {ATTR_SOUND_OUTPUT: self._client.sound_output} + return self._supported_features @cmd async def async_turn_off(self) -> None: diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index 46f0086e0f6a0..82e6185618790 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -7,7 +7,7 @@ from aiowebostv import WebOsClient, WebOsTvPairError from homeassistant.components.notify import ATTR_DATA, BaseNotificationService -from homeassistant.const import CONF_ICON +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -46,8 +46,8 @@ async def async_send_message(self, message: str = "", **kwargs: Any) -> None: if not self._client.is_connected(): await self._client.connect() - data = kwargs.get(ATTR_DATA) - icon_path = data.get(CONF_ICON) if data else None + data = kwargs[ATTR_DATA] + icon_path = data.get(ATTR_ICON) if data else None await self._client.send_message(message, icon_path=icon_path) except WebOsTvPairError: _LOGGER.error("Pairing with TV failed") diff --git a/homeassistant/components/webostv/translations/bg.json b/homeassistant/components/webostv/translations/bg.json index cb9aea4f85dbe..28092bd8b8c38 100644 --- a/homeassistant/components/webostv/translations/bg.json +++ b/homeassistant/components/webostv/translations/bg.json @@ -3,6 +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" }, + "flow_title": "LG webOS Smart TV", "step": { "user": { "data": { diff --git a/homeassistant/components/webostv/translations/cs.json b/homeassistant/components/webostv/translations/cs.json index ef9650f28ae99..4ab388e95df23 100644 --- a/homeassistant/components/webostv/translations/cs.json +++ b/homeassistant/components/webostv/translations/cs.json @@ -2,7 +2,46 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "error_pairing": "P\u0159ipojeno k televizoru LG se syst\u00e9mem webOS, ale nesp\u00e1rov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit, zapn\u011bte pros\u00edm televizor nebo zkontrolujte IP adresu" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Klikn\u011bte na tla\u010d\u00edtko Odeslat a p\u0159ijm\u011bte \u017e\u00e1dost o sp\u00e1rov\u00e1n\u00ed na va\u0161em televizoru.\n\n![Image] (/static/images/config_webos.png)", + "title": "P\u00e1rov\u00e1n\u00ed televize se syst\u00e9mem webOS" + }, + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no" + }, + "description": "Zapn\u011bte televizi, vypl\u0148te n\u00e1sleduj\u00edc\u00ed pole, klikn\u011bte na Odeslat", + "title": "P\u0159ipojen\u00ed k televizoru se syst\u00e9mem webOS" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Za\u0159\u00edzen\u00ed se m\u00e1 zapnout" + } + }, + "options": { + "error": { + "cannot_retrieve": "Nelze na\u010d\u00edst seznam zdroj\u016f. Zkontrolujte, zda je za\u0159\u00edzen\u00ed zapnut\u00e9", + "script_not_found": "Skript nebyl nalezen" + }, + "step": { + "init": { + "data": { + "sources": "Seznam zdroj\u016f" + }, + "description": "V\u00fdb\u011br povolen\u00fdch zdroj\u016f", + "title": "Mo\u017enosti pro chytrou televizi se syst\u00e9mem webOS" + } } } } \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/el.json b/homeassistant/components/webostv/translations/el.json index 115f2d4cdf83a..9b5566b1ea4e9 100644 --- a/homeassistant/components/webostv/translations/el.json +++ b/homeassistant/components/webostv/translations/el.json @@ -1,6 +1,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", + "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", "error_pairing": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5 LG webOS TV \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03b6\u03b5\u03c5\u03c7\u03b8\u03b5\u03af" }, "error": { @@ -14,6 +16,7 @@ }, "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" }, "description": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7, \u03c3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b5\u03b4\u03af\u03b1 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae", diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json new file mode 100644 index 0000000000000..d15f31b514c53 --- /dev/null +++ b/homeassistant/components/webostv/translations/es.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "error_pairing": "Conectado a LG webOS TV pero no emparejado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, encienda el televisor o compruebe la direcci\u00f3n IP" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu televisor.\n\n![Image](/static/images/config_webos.png)", + "title": "Emparejamiento de webOS TV" + }, + "user": { + "description": "Encienda la televisi\u00f3n, rellene los siguientes campos y haga clic en enviar", + "title": "Conectarse a webOS TV" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Se solicita el encendido del dispositivo" + } + }, + "options": { + "error": { + "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farese de que el dispositivo est\u00e1 encendido", + "script_not_found": "Script no encontrado" + }, + "step": { + "init": { + "data": { + "sources": "Lista de fuentes" + }, + "description": "Seleccionar fuentes habilitadas", + "title": "Opciones para webOS Smart TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/fr.json b/homeassistant/components/webostv/translations/fr.json index 82b4196eb751c..bccb1c3aa3ccc 100644 --- a/homeassistant/components/webostv/translations/fr.json +++ b/homeassistant/components/webostv/translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "error_pairing": "Connect\u00e9 au t\u00e9l\u00e9viseur LG webOS mais non jumel\u00e9" }, "error": { @@ -36,7 +38,9 @@ "init": { "data": { "sources": "Liste des sources" - } + }, + "description": "S\u00e9lectionnez les sources activ\u00e9es", + "title": "Options pour webOS Smart TV" } } } diff --git a/homeassistant/components/webostv/translations/id.json b/homeassistant/components/webostv/translations/id.json index 44ee9452fef15..81bc9f86bf696 100644 --- a/homeassistant/components/webostv/translations/id.json +++ b/homeassistant/components/webostv/translations/id.json @@ -2,14 +2,45 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "error_pairing": "Terhubung ke LG webOS TV tetapi tidak dipasangkan" }, + "error": { + "cannot_connect": "Gagal terhubung, nyalakan TV atau periksa alamat IP" + }, + "flow_title": "LG webOS Smart TV", "step": { + "pairing": { + "description": "Klik kirim dan terima permintaan pemasangan di TV Anda. \n\n![Image](/static/images/config_webos.png)", + "title": "Pasangan webOS TV" + }, "user": { "data": { "host": "Host", "name": "Nama" - } + }, + "description": "Nyalakan TV, isi bidang berikut lalu klik kirimkan", + "title": "Hubungkan ke webOS TV" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Perangkat diminta untuk dinyalakan" + } + }, + "options": { + "error": { + "cannot_retrieve": "Tidak dapat mengambil daftar sumber. Pastikan perangkat dihidupkan", + "script_not_found": "Skrip tidak ditemukan" + }, + "step": { + "init": { + "data": { + "sources": "Daftar sumber" + }, + "description": "Pilih sumber yang diaktifkan", + "title": "Opsi untuk webOS Smart TV" } } } diff --git a/homeassistant/components/webostv/translations/lv.json b/homeassistant/components/webostv/translations/lv.json new file mode 100644 index 0000000000000..676af9e30aaf7 --- /dev/null +++ b/homeassistant/components/webostv/translations/lv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "sources": "Avotu saraksts" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/nl.json b/homeassistant/components/webostv/translations/nl.json index f7325c02cff85..2a9fb59e1c121 100644 --- a/homeassistant/components/webostv/translations/nl.json +++ b/homeassistant/components/webostv/translations/nl.json @@ -39,7 +39,8 @@ "data": { "sources": "Bronnenlijst" }, - "description": "Selecteer ingeschakelde bronnen" + "description": "Selecteer ingeschakelde bronnen", + "title": "Opties voor WebOS Smart TV" } } } diff --git a/homeassistant/components/webostv/translations/pl.json b/homeassistant/components/webostv/translations/pl.json index 6f974191dc744..9792994653678 100644 --- a/homeassistant/components/webostv/translations/pl.json +++ b/homeassistant/components/webostv/translations/pl.json @@ -1,25 +1,32 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "error_pairing": "Po\u0142\u0105czono z telewizorem LG webOS, ale nie sparowano" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107, w\u0142\u0105cz telewizor lub sprawd\u017a adres IP" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, w\u0142\u0105cz telewizor lub sprawd\u017a adres IP" }, - "flow_title": "Smart telewizor LG webOS", + "flow_title": "LG webOS Smart TV", "step": { "pairing": { + "description": "Kliknij \"Zatwierd\u017a\" i zaakceptuj \u017c\u0105danie parowania na swoim telewizorze. \n\n![Obraz](/static/images/config_webos.png)", "title": "Parowanie webOS TV" }, "user": { - "description": "W\u0142\u0105cz telewizor, wype\u0142nij wymgane pola i kliknij prze\u015blij", - "title": "Po\u0142\u0105cz z webOS TV" + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa" + }, + "description": "W\u0142\u0105cz telewizor, wype\u0142nij wymagane pola i kliknij \"Zatwierd\u017a\"", + "title": "Po\u0142\u0105czenie z webOS TV" } } }, "device_automation": { "trigger_type": { - "webostv.turn_on": "Urz\u0105dzenie jest poproszone o w\u0142\u0105czenie si\u0119" + "webostv.turn_on": "urz\u0105dzenie zostanie poproszone o w\u0142\u0105czenie" } }, "options": { diff --git a/homeassistant/components/webostv/translations/pt-BR.json b/homeassistant/components/webostv/translations/pt-BR.json new file mode 100644 index 0000000000000..9eddde059a8cb --- /dev/null +++ b/homeassistant/components/webostv/translations/pt-BR.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "error_pairing": "Conectado \u00e0 LG webOS TV, mas n\u00e3o emparelhado" + }, + "error": { + "cannot_connect": "Falha ao conectar, ligue sua TV ou verifique o endere\u00e7o IP" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Clique em enviar e aceitar a solicita\u00e7\u00e3o de emparelhamento em sua TV.\n\n! [Imagem] (/est\u00e1tica/imagens/config_webos.png)", + "title": "Emparelhamento de TV webOS" + }, + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + }, + "description": "Ligue a TV, preencha os campos a seguir clique em enviar", + "title": "Conecte-se \u00e0 webOS TV" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "O dispositivo \u00e9 solicitado para ligar" + } + }, + "options": { + "error": { + "cannot_retrieve": "N\u00e3o foi poss\u00edvel recuperar a lista de fontes. Verifique se o dispositivo est\u00e1 ligado", + "script_not_found": "Script n\u00e3o encontrado" + }, + "step": { + "init": { + "data": { + "sources": "Lista de fontes" + }, + "description": "Selecionar fontes habilitadas", + "title": "Op\u00e7\u00f5es para WebOS Smart TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/sk.json b/homeassistant/components/webostv/translations/sk.json new file mode 100644 index 0000000000000..5768568606545 --- /dev/null +++ b/homeassistant/components/webostv/translations/sk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "error_pairing": "Pripojen\u00e9 k telev\u00edzoru LG so syst\u00e9mom webOS, ale nesp\u00e1rovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165, pros\u00edm, zapnite telev\u00edzor alebo skontrolujte IP adresu" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Kliknite na Odosla\u0165 a prijmite \u017eiados\u0165 o p\u00e1rovanie na telev\u00edzore.\n\n![Image](/static/images/config_webos.png)", + "title": "Sp\u00e1rovanie TV so syst\u00e9mom webOS" + }, + "user": { + "data": { + "name": "N\u00e1zov" + }, + "description": "Zapnite TV, vypl\u0148te nasleduj\u00face polia, kliknite na Odosla\u0165", + "title": "Pripojenie k TV so syst\u00e9mom webOS" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Zariadenie sa m\u00e1 zapn\u00fa\u0165" + } + }, + "options": { + "error": { + "cannot_retrieve": "Nie je mo\u017en\u00e9 na\u010d\u00edta\u0165 zoznam zdrojov. Skontrolujte, \u010di je zariadenie zapnut\u00e9", + "script_not_found": "Skript sa nena\u0161iel" + }, + "step": { + "init": { + "data": { + "sources": "Zoznam zdrojov" + }, + "description": "V\u00fdber povolen\u00fdch zdrojov", + "title": "Mo\u017enosti pre Smart TV so syst\u00e9mom webOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/tr.json b/homeassistant/components/webostv/translations/tr.json index 94e5d3abef3b8..c4f0f5c65c483 100644 --- a/homeassistant/components/webostv/translations/tr.json +++ b/homeassistant/components/webostv/translations/tr.json @@ -32,7 +32,7 @@ "options": { "error": { "cannot_retrieve": "Kaynak listesi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun", - "script_not_found": "Komut dosyas\u0131 bulunamad\u0131" + "script_not_found": "Senaryo bulunamad\u0131" }, "step": { "init": { diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4020601dc3f0f..abc37dd2a0a64 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,8 +3,9 @@ import asyncio from collections.abc import Callable +import datetime as dt import json -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -33,6 +34,10 @@ from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import const, decorators, messages from .connection import ActiveConnection @@ -62,6 +67,7 @@ def async_register_commands( async_reg(hass, handle_subscribe_trigger) async_reg(hass, handle_test_condition) async_reg(hass, handle_unsubscribe_events) + async_reg(hass, handle_validate_config) def pong_message(iden: int) -> dict[str, Any]: @@ -115,7 +121,7 @@ def forward_events(event: Event) -> None: event_type, forward_events ) - connection.send_message(messages.result_message(msg["id"])) + connection.send_result(msg["id"]) @callback @@ -138,7 +144,7 @@ def forward_bootstrap_integrations(message: dict[str, Any]) -> None: hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations ) - connection.send_message(messages.result_message(msg["id"])) + connection.send_result(msg["id"]) @callback @@ -156,13 +162,9 @@ def handle_unsubscribe_events( if subscription in connection.subscriptions: connection.subscriptions.pop(subscription)() - connection.send_message(messages.result_message(msg["id"])) + connection.send_result(msg["id"]) else: - connection.send_message( - messages.error_message( - msg["id"], const.ERR_NOT_FOUND, "Subscription not found." - ) - ) + connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Subscription not found.") @decorators.websocket_command( @@ -195,36 +197,20 @@ async def handle_call_service( context, target=target, ) - connection.send_message( - messages.result_message(msg["id"], {"context": context}) - ) + connection.send_result(msg["id"], {"context": context}) except ServiceNotFound as err: if err.domain == msg["domain"] and err.service == msg["service"]: - connection.send_message( - messages.error_message( - msg["id"], const.ERR_NOT_FOUND, "Service not found." - ) - ) + connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Service not found.") else: - connection.send_message( - messages.error_message( - msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err) - ) - ) + connection.send_error(msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err)) except vol.Invalid as err: - connection.send_message( - messages.error_message(msg["id"], const.ERR_INVALID_FORMAT, str(err)) - ) + connection.send_error(msg["id"], const.ERR_INVALID_FORMAT, str(err)) except HomeAssistantError as err: connection.logger.exception(err) - connection.send_message( - messages.error_message(msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err)) - ) + connection.send_error(msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err)) except Exception as err: # pylint: disable=broad-except connection.logger.exception(err) - connection.send_message( - messages.error_message(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) - ) + connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) @callback @@ -243,7 +229,35 @@ def handle_get_states( if entity_perm(state.entity_id, "read") ] - connection.send_message(messages.result_message(msg["id"], states)) + # JSON serialize here so we can recover if it blows up due to the + # state machine containing unserializable data. This command is required + # to succeed for the UI to show. + response = messages.result_message(msg["id"], states) + try: + connection.send_message(const.JSON_DUMP(response)) + return + except (ValueError, TypeError): + connection.logger.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(response, dump=const.JSON_DUMP) + ), + ) + del response + + # If we can't serialize, we'll filter out unserializable states + serialized = [] + for state in states: + try: + serialized.append(const.JSON_DUMP(state)) + except (ValueError, TypeError): + # Error is already logged above + pass + + # We now have partially serialized states. Craft some JSON. + response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) + connection.send_message(response2) @decorators.websocket_command({vol.Required("type"): "get_services"}) @@ -253,7 +267,7 @@ async def handle_get_services( ) -> None: """Handle get services command.""" descriptions = await async_get_all_descriptions(hass) - connection.send_message(messages.result_message(msg["id"], descriptions)) + connection.send_result(msg["id"], descriptions) @callback @@ -262,7 +276,7 @@ def handle_get_config( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle get config command.""" - connection.send_message(messages.result_message(msg["id"], hass.config.as_dict())) + connection.send_result(msg["id"], hass.config.as_dict()) @decorators.websocket_command({vol.Required("type"): "manifest/list"}) @@ -305,7 +319,9 @@ async def handle_integration_setup_info( msg["id"], [ {"domain": integration, "seconds": timedelta.total_seconds()} - for integration, timedelta in hass.data[DATA_SETUP_TIME].items() + for integration, timedelta in cast( + dict[str, dt.timedelta], hass.data[DATA_SETUP_TIME] + ).items() ], ) @@ -343,7 +359,7 @@ async def handle_render_template( if timeout: try: timed_out = await template_obj.async_render_will_timeout( - timeout, strict=msg["strict"] + timeout, variables, strict=msg["strict"] ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) @@ -414,7 +430,7 @@ def handle_entity_source( if entity_perm(entity_id, "read") } - connection.send_message(messages.result_message(msg["id"], sources)) + connection.send_result(msg["id"], sources) return sources = {} @@ -464,7 +480,9 @@ def forward_triggers( msg["id"], {"variables": variables, "context": context} ) connection.send_message( - json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) + json.dumps( + message, cls=ExtendedJSONEncoder, allow_nan=False, separators=(",", ":") + ) ) connection.subscriptions[msg["id"]] = ( @@ -532,7 +550,7 @@ async def handle_execute_script( context = connection.context(msg) script_obj = Script(hass, msg["sequence"], f"{const.DOMAIN} script", const.DOMAIN) await script_obj.async_run(msg.get("variables"), context=context) - connection.send_message(messages.result_message(msg["id"], {"context": context})) + connection.send_result(msg["id"], {"context": context}) @decorators.websocket_command( @@ -552,3 +570,40 @@ async def handle_fire_event( hass.bus.async_fire(msg["event_type"], msg.get("event_data"), context=context) connection.send_result(msg["id"], {"context": context}) + + +@decorators.websocket_command( + { + vol.Required("type"): "validate_config", + vol.Optional("trigger"): cv.match_all, + vol.Optional("condition"): cv.match_all, + vol.Optional("action"): cv.match_all, + } +) +@decorators.async_response +async def handle_validate_config( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle validate config command.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from homeassistant.helpers import condition, script, trigger + + result = {} + + for key, schema, validator in ( + ("trigger", cv.TRIGGER_SCHEMA, trigger.async_validate_trigger_config), + ("condition", cv.CONDITION_SCHEMA, condition.async_validate_condition_config), + ("action", cv.SCRIPT_SCHEMA, script.async_validate_actions_config), + ): + if key not in msg: + continue + + try: + await validator(hass, schema(msg[key])) # type: ignore + except vol.Invalid as err: + result[key] = {"valid": False, "error": str(err)} + else: + result[key] = {"valid": True, "error": None} + + connection.send_result(msg["id"], result) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9428d6fd87d3e..6c5615ad253b8 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -53,4 +53,6 @@ # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, allow_nan=False) +JSON_DUMP: Final = partial( + json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") +) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index d0643ed51a98b..d048a59d38c54 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -13,5 +13,6 @@ "models": ["Socket", "Wemo"] }, "codeowners": ["@esev"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["pywemo"] } diff --git a/homeassistant/components/wemo/translations/el.json b/homeassistant/components/wemo/translations/el.json new file mode 100644 index 0000000000000..b07fd2a338627 --- /dev/null +++ b/homeassistant/components/wemo/translations/el.json @@ -0,0 +1,18 @@ +{ + "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." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Wemo;" + } + } + }, + "device_automation": { + "trigger_type": { + "long_press": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af Wemo \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 2 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/pt-BR.json b/homeassistant/components/wemo/translations/pt-BR.json index c14cb64bf4e8a..59e6bf2e3b08e 100644 --- a/homeassistant/components/wemo/translations/pt-BR.json +++ b/homeassistant/components/wemo/translations/pt-BR.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Wemo encontrado na rede.", - "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Wemo \u00e9 poss\u00edvel." + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { "description": "Voc\u00ea quer configurar o Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "O bot\u00e3o Wemo foi pressionado por 2 segundos." + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/uk.json b/homeassistant/components/wemo/translations/uk.json index 1217d66423424..2a705d370f4d9 100644 --- a/homeassistant/components/wemo/translations/uk.json +++ b/homeassistant/components/wemo/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json index a9a4a2a8b20c5..05e66acedcf7c 100644 --- a/homeassistant/components/wemo/translations/zh-Hant.json +++ b/homeassistant/components/wemo/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 9df10f32931e9..ce5c76c72f049 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -9,5 +9,6 @@ "codeowners": [ "@abmantis" ], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["whirlpool"] } diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json new file mode 100644 index 0000000000000..9ffc0f96a831b --- /dev/null +++ b/homeassistant/components/whirlpool/translations/el.json @@ -0,0 +1,17 @@ +{ + "config": { + "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": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nb.json b/homeassistant/components/whirlpool/translations/nb.json new file mode 100644 index 0000000000000..847c45368fd80 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/nb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index acfb9e2178a77..8cbb0f6f502a2 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -5,5 +5,6 @@ "requirements": ["whois==0.9.13"], "config_flow": true, "codeowners": ["@frenck"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["whois"] } diff --git a/homeassistant/components/whois/translations/el.json b/homeassistant/components/whois/translations/el.json index b116200db5787..bcf941fe39bde 100644 --- a/homeassistant/components/whois/translations/el.json +++ b/homeassistant/components/whois/translations/el.json @@ -1,5 +1,8 @@ { "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": { "unexpected_response": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b1\u03c0\u03ac\u03bd\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae whois", "unknown_date_format": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae \u03b7\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae whois", diff --git a/homeassistant/components/whois/translations/id.json b/homeassistant/components/whois/translations/id.json index fa8cd415378a7..ae926fd522ffb 100644 --- a/homeassistant/components/whois/translations/id.json +++ b/homeassistant/components/whois/translations/id.json @@ -2,6 +2,19 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "unexpected_response": "Respons tak terduga dari server whois", + "unknown_date_format": "Format tanggal tidak diketahui dalam respons server whois", + "unknown_tld": "TLD yang diberikan tidak diketahui atau tidak tersedia untuk integrasi ini", + "whois_command_failed": "Perintah whois gagal: tidak dapat mengambil informasi whois" + }, + "step": { + "user": { + "data": { + "domain": "Nama domain" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/whois/translations/ja.json b/homeassistant/components/whois/translations/ja.json index 7102e95a25d5f..66a7868a26597 100644 --- a/homeassistant/components/whois/translations/ja.json +++ b/homeassistant/components/whois/translations/ja.json @@ -3,6 +3,12 @@ "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, + "error": { + "unexpected_response": "Whois\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u306e\u4e88\u671f\u3057\u306a\u3044\u5fdc\u7b54", + "unknown_date_format": "Whois\u30b5\u30fc\u30d0\u30fc\u306e\u5fdc\u7b54\u3067\u4e0d\u660e\u306a\u65e5\u4ed8\u30d5\u30a9\u30fc\u30de\u30c3\u30c8", + "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", + "whois_command_failed": "Whois\u30b3\u30de\u30f3\u30c9\u304c\u5931\u6557\u3057\u307e\u3057\u305f: whois\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/whois/translations/pl.json b/homeassistant/components/whois/translations/pl.json index 621be0d70cc8e..f0624f627bbdc 100644 --- a/homeassistant/components/whois/translations/pl.json +++ b/homeassistant/components/whois/translations/pl.json @@ -4,9 +4,10 @@ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" }, "error": { + "unexpected_response": "Nieoczekiwana odpowied\u017a z serwera whois", "unknown_date_format": "Nieznany format daty w odpowiedzi serwera whois", "unknown_tld": "Podana domena TLD jest nieznana lub niedost\u0119pna dla tej integracji", - "whois_command_failed": "Komenda Whois nie powiod\u0142a si\u0119: nie mo\u017cna pobra\u0107 informacji whois" + "whois_command_failed": "Komenda Whois nie powiod\u0142a si\u0119: nie mo\u017cna pobra\u0107 informacji z whois" }, "step": { "user": { diff --git a/homeassistant/components/whois/translations/pt-BR.json b/homeassistant/components/whois/translations/pt-BR.json new file mode 100644 index 0000000000000..cf4b5334c2062 --- /dev/null +++ b/homeassistant/components/whois/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "unexpected_response": "Resposta inesperada do servidor whois", + "unknown_date_format": "Formato de data desconhecido na resposta do servidor whois", + "unknown_tld": "O TLD fornecido \u00e9 desconhecido ou n\u00e3o est\u00e1 dispon\u00edvel para esta integra\u00e7\u00e3o", + "whois_command_failed": "O comando Whois falhou: n\u00e3o foi poss\u00edvel recuperar informa\u00e7\u00f5es whois" + }, + "step": { + "user": { + "data": { + "domain": "Nome do dom\u00ednio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whois/translations/zh-Hans.json b/homeassistant/components/whois/translations/zh-Hans.json new file mode 100644 index 0000000000000..821295d2c822f --- /dev/null +++ b/homeassistant/components/whois/translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "unexpected_response": "\u6765\u81ea Whois \u7684\u672a\u77e5\u9519\u8bef", + "unknown_date_format": "Whois \u670d\u52a1\u5668\u8fd4\u56de\u672a\u77e5\u7684\u65e5\u671f\u683c\u5f0f", + "whois_command_failed": "\u6267\u884c Whois \u547d\u4ee4\u5931\u8d25\uff0c\u65e0\u6cd5\u68c0\u7d22\u5230 Whois \u4fe1\u606f" + }, + "step": { + "user": { + "data": { + "domain": "\u57df\u540d\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index ab5bda8dd1d96..f2d11d53862b5 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/wiffi/manifest.json b/homeassistant/components/wiffi/manifest.json index 58d0f9778d709..e28062c74c075 100644 --- a/homeassistant/components/wiffi/manifest.json +++ b/homeassistant/components/wiffi/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/wiffi", "requirements": ["wiffi==1.1.0"], "codeowners": ["@mampfes"], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["wiffi"] } diff --git a/homeassistant/components/wiffi/translations/el.json b/homeassistant/components/wiffi/translations/el.json index ae15bb06d1960..100dc55ecaee6 100644 --- a/homeassistant/components/wiffi/translations/el.json +++ b/homeassistant/components/wiffi/translations/el.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + "addr_in_use": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", + "already_configured": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af.", + "start_server_failed": "\u0397 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." + }, + "step": { + "user": { + "data": { + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae TCP \u03b3\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 WIFFI" + } } }, "options": { diff --git a/homeassistant/components/wiffi/translations/lv.json b/homeassistant/components/wiffi/translations/lv.json new file mode 100644 index 0000000000000..b6f2cec8396fa --- /dev/null +++ b/homeassistant/components/wiffi/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Servera ports jau ir konfigur\u0113ts." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/pt-BR.json b/homeassistant/components/wiffi/translations/pt-BR.json index cbe6c6f78e769..226e5399cdd2c 100644 --- a/homeassistant/components/wiffi/translations/pt-BR.json +++ b/homeassistant/components/wiffi/translations/pt-BR.json @@ -2,15 +2,25 @@ "config": { "abort": { "addr_in_use": "Porta do servidor j\u00e1 em uso.", + "already_configured": "A porta do servidor j\u00e1 est\u00e1 configurada.", "start_server_failed": "Falha ao iniciar o servidor." }, "step": { "user": { "data": { - "port": "Porta do servidor" + "port": "Porta" }, "title": "Configurar servidor TCP para dispositivos WIFFI" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Tempo limite (minutos)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/sk.json b/homeassistant/components/wiffi/translations/sk.json new file mode 100644 index 0000000000000..892b8b2cd9124 --- /dev/null +++ b/homeassistant/components/wiffi/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index fec9fdb6c6a20..972de72a9c99c 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -11,5 +11,6 @@ ], "codeowners": ["@leofig-rj"], "quality_scale": "silver", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pywilight"] } diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index 3f5afd4d729ce..e83a8386341e6 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -1,6 +1,7 @@ { "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", "not_supported_device": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf WiLight \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd", "not_wilight_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 WiLight" }, diff --git a/homeassistant/components/wilight/translations/pt-BR.json b/homeassistant/components/wilight/translations/pt-BR.json new file mode 100644 index 0000000000000..5da689b9b74e3 --- /dev/null +++ b/homeassistant/components/wilight/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "not_supported_device": "Este WiLight n\u00e3o \u00e9 suportado atualmente", + "not_wilight_device": "Este dispositivo n\u00e3o \u00e9 WiLight" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Voc\u00ea deseja configurar WiLight {name}?\n\nEle suporta: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index 6074b64d6649c..881ac34c93f57 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/wirelesstag", "requirements": ["wirelesstagpy==0.8.1"], "codeowners": ["@sergeymaysak"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["wirelesstagpy"] } diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d1c867cd4e645..f15045b98da07 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -3,8 +3,18 @@ "name": "Withings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", - "requirements": ["withings-api==2.3.2"], - "dependencies": ["http", "webhook"], - "codeowners": ["@vangorra"], - "iot_class": "cloud_polling" -} + "requirements": [ + "withings-api==2.4.0" + ], + "dependencies": [ + "http", + "webhook" + ], + "codeowners": [ + "@vangorra" + ], + "iot_class": "cloud_polling", + "loggers": [ + "withings_api" + ] +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/el.json b/homeassistant/components/withings/translations/el.json index 78528a8898b4c..dc284985ee273 100644 --- a/homeassistant/components/withings/translations/el.json +++ b/homeassistant/components/withings/translations/el.json @@ -1,18 +1,32 @@ { "config": { + "abort": { + "already_configured": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb.", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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} )" + }, "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Withings." }, + "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" + }, "flow_title": "{profile}", "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, "profile": { "data": { "profile": "\u039f\u03bd\u03bf\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb" }, - "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." + "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." + "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/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json index f87b8b6457642..6a067498f1e03 100644 --- a/homeassistant/components/withings/translations/pt-BR.json +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -1,7 +1,33 @@ { "config": { + "abort": { + "already_configured": "Configura\u00e7\u00e3o atualizada para o perfil.", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "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})" + }, "create_entry": { "default": "Autenticado com sucesso no Withings." + }, + "error": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "flow_title": "{profile}", + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "profile": { + "data": { + "profile": "Nome do perfil" + }, + "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" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py new file mode 100644 index 0000000000000..d739c571c8b56 --- /dev/null +++ b/homeassistant/components/wiz/__init__.py @@ -0,0 +1,128 @@ +"""WiZ Platform integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any + +from pywizlight import PilotParser, wizlight +from pywizlight.bulb import PIR_SOURCE + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + DISCOVER_SCAN_TIMEOUT, + DISCOVERY_INTERVAL, + DOMAIN, + SIGNAL_WIZ_PIR, + WIZ_CONNECT_EXCEPTIONS, + WIZ_EXCEPTIONS, +) +from .discovery import async_discover_devices, async_trigger_discovery +from .models import WizData + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.NUMBER, Platform.SWITCH] + +REQUEST_REFRESH_DELAY = 0.35 + + +async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: + """Set up the wiz integration.""" + + async def _async_discovery(*_: Any) -> None: + async_trigger_discovery( + hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT) + ) + + asyncio.create_task(_async_discovery()) + async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the wiz integration from a config entry.""" + ip_address = entry.data[CONF_HOST] + _LOGGER.debug("Get bulb with IP: %s", ip_address) + bulb = wizlight(ip_address) + try: + scenes = await bulb.getSupportedScenes() + await bulb.getMac() + except WIZ_CONNECT_EXCEPTIONS as err: + await bulb.async_close() + raise ConfigEntryNotReady(f"{ip_address}: {err}") from err + + if bulb.mac != entry.unique_id: + # The ip address of the bulb has changed and its likely offline + # and another WiZ device has taken the IP. Avoid setting up + # since its the wrong device. As soon as the device comes back + # online the ip will get updated and setup will proceed. + raise ConfigEntryNotReady( + "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" + ) + + async def _async_update() -> None: + """Update the WiZ device.""" + try: + await bulb.updateState() + except WIZ_EXCEPTIONS as ex: + raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex + + coordinator = DataUpdateCoordinator( + hass=hass, + logger=_LOGGER, + name=entry.title, + update_interval=timedelta(seconds=15), + update_method=_async_update, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady as err: + await bulb.async_close() + raise err + + async def _async_shutdown_on_stop(event: Event) -> None: + await bulb.async_close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_on_stop) + ) + + @callback + def _async_push_update(state: PilotParser) -> None: + """Receive a push update.""" + _LOGGER.debug("%s: Got push update: %s", bulb.mac, state.pilotResult) + coordinator.async_set_updated_data(None) + if state.get_source() == PIR_SOURCE: + async_dispatcher_send(hass, SIGNAL_WIZ_PIR.format(bulb.mac)) + + await bulb.start_push(_async_push_update) + bulb.set_discovery_callback(lambda bulb: async_trigger_discovery(hass, [bulb])) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData( + coordinator=coordinator, bulb=bulb, scenes=scenes + ) + hass.config_entries.async_setup_platforms(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): + data: WizData = hass.data[DOMAIN].pop(entry.entry_id) + await data.bulb.async_close() + return unload_ok diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py new file mode 100644 index 0000000000000..1ecb31252155b --- /dev/null +++ b/homeassistant/components/wiz/binary_sensor.py @@ -0,0 +1,81 @@ +"""WiZ integration binary sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable + +from pywizlight.bulb import PIR_SOURCE + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, SIGNAL_WIZ_PIR +from .entity import WizEntity +from .models import WizData + +OCCUPANCY_UNIQUE_ID = "{}_occupancy" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the WiZ binary sensor platform.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + mac = wiz_data.bulb.mac + + if er.async_get(hass).async_get_entity_id( + Platform.BINARY_SENSOR, DOMAIN, OCCUPANCY_UNIQUE_ID.format(mac) + ): + async_add_entities([WizOccupancyEntity(wiz_data, entry.title)]) + return + + cancel_dispatcher: Callable[[], None] | None = None + + @callback + def _async_add_occupancy_sensor() -> None: + nonlocal cancel_dispatcher + assert cancel_dispatcher is not None + cancel_dispatcher() + cancel_dispatcher = None + async_add_entities([WizOccupancyEntity(wiz_data, entry.title)]) + + cancel_dispatcher = async_dispatcher_connect( + hass, SIGNAL_WIZ_PIR.format(mac), _async_add_occupancy_sensor + ) + + @callback + def _async_cancel_dispatcher() -> None: + nonlocal cancel_dispatcher + if cancel_dispatcher is not None: + cancel_dispatcher() + cancel_dispatcher = None + + entry.async_on_unload(_async_cancel_dispatcher) + + +class WizOccupancyEntity(WizEntity, BinarySensorEntity): + """Representation of WiZ Occupancy sensor.""" + + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + + def __init__(self, wiz_data: WizData, name: str) -> None: + """Initialize an WiZ device.""" + super().__init__(wiz_data, name) + self._attr_unique_id = OCCUPANCY_UNIQUE_ID.format(self._device.mac) + self._attr_name = f"{name} Occupancy" + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + if self._device.state.get_source() == PIR_SOURCE: + self._attr_is_on = self._device.status diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py new file mode 100644 index 0000000000000..7d86502784c24 --- /dev/null +++ b/homeassistant/components/wiz/config_flow.py @@ -0,0 +1,190 @@ +"""Config flow for WiZ Platform.""" +from __future__ import annotations + +import logging +from typing import Any + +from pywizlight import wizlight +from pywizlight.discovery import DiscoveredBulb +from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.util.network import is_ip_address + +from .const import DEFAULT_NAME, DISCOVER_SCAN_TIMEOUT, DOMAIN, WIZ_CONNECT_EXCEPTIONS +from .discovery import async_discover_devices +from .utils import _short_mac, name_from_bulb_type_and_mac + +_LOGGER = logging.getLogger(__name__) + +CONF_DEVICE = "device" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for WiZ.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_device: DiscoveredBulb | None = None + self._discovered_devices: dict[str, DiscoveredBulb] = {} + self._name: str | None = None + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + self._discovered_device = DiscoveredBulb( + discovery_info.ip, discovery_info.macaddress + ) + return await self._async_handle_discovery() + + async def async_step_integration_discovery( + self, discovery_info: dict[str, str] + ) -> FlowResult: + """Handle integration discovery.""" + self._discovered_device = DiscoveredBulb( + discovery_info["ip_address"], discovery_info["mac_address"] + ) + return await self._async_handle_discovery() + + async def _async_handle_discovery(self) -> FlowResult: + """Handle any discovery.""" + device = self._discovered_device + assert device is not None + _LOGGER.debug("Discovered device: %s", device) + ip_address = device.ip_address + mac = device.mac_address + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured(updates={CONF_HOST: ip_address}) + await self._async_connect_discovered_or_abort() + return await self.async_step_discovery_confirm() + + async def _async_connect_discovered_or_abort(self) -> None: + """Connect to the device and verify its responding.""" + device = self._discovered_device + assert device is not None + bulb = wizlight(device.ip_address) + try: + bulbtype = await bulb.get_bulbtype() + except WIZ_CONNECT_EXCEPTIONS as ex: + _LOGGER.debug( + "Failed to connect to %s during discovery: %s", + device.ip_address, + ex, + exc_info=True, + ) + raise AbortFlow("cannot_connect") from ex + self._name = name_from_bulb_type_and_mac(bulbtype, device.mac_address) + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + assert self._name is not None + ip_address = self._discovered_device.ip_address + if user_input is not None: + # Make sure the device is still there and + # update the name if the firmware has auto + # updated since discovery + await self._async_connect_discovered_or_abort() + return self.async_create_entry( + title=self._name, + data={CONF_HOST: ip_address}, + ) + + self._set_confirm_only() + placeholders = {"name": self._name, "host": ip_address} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders=placeholders, + data_schema=vol.Schema({}), + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the step to pick discovered device.""" + if user_input is not None: + device = self._discovered_devices[user_input[CONF_DEVICE]] + await self.async_set_unique_id(device.mac_address, raise_on_progress=False) + bulb = wizlight(device.ip_address) + try: + bulbtype = await bulb.get_bulbtype() + except WIZ_CONNECT_EXCEPTIONS: + return self.async_abort(reason="cannot_connect") + else: + return self.async_create_entry( + title=name_from_bulb_type_and_mac(bulbtype, device.mac_address), + data={CONF_HOST: device.ip_address}, + ) + + current_unique_ids = self._async_current_ids() + current_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + } + discovered_devices = await async_discover_devices( + self.hass, DISCOVER_SCAN_TIMEOUT + ) + self._discovered_devices = { + device.mac_address: device for device in discovered_devices + } + devices_name = { + mac: f"{DEFAULT_NAME} {_short_mac(mac)} ({device.ip_address})" + for mac, device in self._discovered_devices.items() + if mac not in current_unique_ids and device.ip_address not in current_hosts + } + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + if not (host := user_input[CONF_HOST]): + return await self.async_step_pick_device() + if not is_ip_address(user_input[CONF_HOST]): + errors["base"] = "no_ip" + else: + bulb = wizlight(host) + try: + bulbtype = await bulb.get_bulbtype() + mac = await bulb.getMac() + except WizLightTimeOutError: + errors["base"] = "bulb_time_out" + except ConnectionRefusedError: + errors["base"] = "cannot_connect" + except WizLightConnectionError: + errors["base"] = "no_wiz_light" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(mac, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]} + ) + name = name_from_bulb_type_and_mac(bulbtype, mac) + return self.async_create_entry( + title=name, + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), + errors=errors, + ) diff --git a/homeassistant/components/wiz/const.py b/homeassistant/components/wiz/const.py new file mode 100644 index 0000000000000..1aeb2ada58082 --- /dev/null +++ b/homeassistant/components/wiz/const.py @@ -0,0 +1,25 @@ +"""Constants for the WiZ Platform integration.""" +from datetime import timedelta + +from pywizlight.exceptions import ( + WizLightConnectionError, + WizLightNotKnownBulb, + WizLightTimeOutError, +) + +DOMAIN = "wiz" +DEFAULT_NAME = "WiZ" + +DISCOVER_SCAN_TIMEOUT = 10 +DISCOVERY_INTERVAL = timedelta(minutes=15) + +WIZ_EXCEPTIONS = ( + OSError, + WizLightTimeOutError, + TimeoutError, + WizLightConnectionError, + ConnectionRefusedError, +) +WIZ_CONNECT_EXCEPTIONS = (WizLightNotKnownBulb, *WIZ_EXCEPTIONS) + +SIGNAL_WIZ_PIR = "wiz_pir_{}" diff --git a/homeassistant/components/wiz/diagnostics.py b/homeassistant/components/wiz/diagnostics.py new file mode 100644 index 0000000000000..4fdf62b3c8ca4 --- /dev/null +++ b/homeassistant/components/wiz/diagnostics.py @@ -0,0 +1,27 @@ +"""Diagnostics support for WiZ.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .models import WizData + +TO_REDACT = {"roomId", "homeId"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + return { + "entry": { + "title": entry.title, + "data": dict(entry.data), + }, + "data": async_redact_data(wiz_data.bulb.diagnostics, TO_REDACT), + } diff --git a/homeassistant/components/wiz/discovery.py b/homeassistant/components/wiz/discovery.py new file mode 100644 index 0000000000000..0b7015643ff58 --- /dev/null +++ b/homeassistant/components/wiz/discovery.py @@ -0,0 +1,55 @@ +"""The wiz integration discovery.""" +from __future__ import annotations + +import asyncio +from dataclasses import asdict +import logging + +from pywizlight.discovery import DiscoveredBulb, find_wizlights + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_discover_devices( + hass: HomeAssistant, timeout: int +) -> list[DiscoveredBulb]: + """Discover wiz devices.""" + broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) + targets = [str(address) for address in broadcast_addrs] + combined_discoveries: dict[str, DiscoveredBulb] = {} + for idx, discovered in enumerate( + await asyncio.gather( + *[find_wizlights(timeout, address) for address in targets], + return_exceptions=True, + ) + ): + if isinstance(discovered, Exception): + _LOGGER.debug("Scanning %s failed with error: %s", targets[idx], discovered) + continue + for device in discovered: + assert isinstance(device, DiscoveredBulb) + combined_discoveries[device.ip_address] = device + + return list(combined_discoveries.values()) + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: list[DiscoveredBulb], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), + ) + ) diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py new file mode 100644 index 0000000000000..9b22d35de7d56 --- /dev/null +++ b/homeassistant/components/wiz/entity.py @@ -0,0 +1,66 @@ +"""WiZ integration entities.""" +from __future__ import annotations + +from abc import abstractmethod +from typing import Any + +from pywizlight.bulblibrary import BulbType + +from homeassistant.const import ATTR_HW_VERSION, ATTR_MODEL +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .models import WizData + + +class WizEntity(CoordinatorEntity, Entity): + """Representation of WiZ entity.""" + + def __init__(self, wiz_data: WizData, name: str) -> None: + """Initialize a WiZ entity.""" + super().__init__(wiz_data.coordinator) + self._device = wiz_data.bulb + bulb_type: BulbType = self._device.bulbtype + self._attr_unique_id = self._device.mac + self._attr_name = name + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._device.mac)}, + name=name, + manufacturer="WiZ", + sw_version=bulb_type.fw_version, + ) + if bulb_type.name is None: + return + hw_data = bulb_type.name.split("_") + board = hw_data.pop(0) + model = hw_data.pop(0) + hw_version = f"{board} {hw_data[0]}" if hw_data else board + self._attr_device_info[ATTR_HW_VERSION] = hw_version + self._attr_device_info[ATTR_MODEL] = model + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + @abstractmethod + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + + +class WizToggleEntity(WizEntity, ToggleEntity): + """Representation of WiZ toggle entity.""" + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + self._attr_is_on = self._device.status + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the device to turn off.""" + await self._device.turn_off() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wiz/light.py b/homeassistant/components/wiz/light.py new file mode 100644 index 0000000000000..9b2d7e6fab45d --- /dev/null +++ b/homeassistant/components/wiz/light.py @@ -0,0 +1,128 @@ +"""WiZ integration light platform.""" +from __future__ import annotations + +from typing import Any + +from pywizlight import PilotBuilder +from pywizlight.bulblibrary import BulbClass, BulbType, Features +from pywizlight.scenes import get_id_from_scene_name + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + SUPPORT_EFFECT, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from .const import DOMAIN +from .entity import WizToggleEntity +from .models import WizData + +RGB_WHITE_CHANNELS_COLOR_MODE = {1: COLOR_MODE_RGBW, 2: COLOR_MODE_RGBWW} + + +def _async_pilot_builder(**kwargs: Any) -> PilotBuilder: + """Create the PilotBuilder for turn on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + + if ATTR_RGBWW_COLOR in kwargs: + return PilotBuilder(brightness=brightness, rgbww=kwargs[ATTR_RGBWW_COLOR]) + + if ATTR_RGBW_COLOR in kwargs: + return PilotBuilder(brightness=brightness, rgbw=kwargs[ATTR_RGBW_COLOR]) + + if ATTR_COLOR_TEMP in kwargs: + return PilotBuilder( + brightness=brightness, + colortemp=color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]), + ) + + if ATTR_EFFECT in kwargs: + scene_id = get_id_from_scene_name(kwargs[ATTR_EFFECT]) + if scene_id == 1000: # rhythm + return PilotBuilder() + return PilotBuilder(brightness=brightness, scene=scene_id) + + return PilotBuilder(brightness=brightness) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the WiZ Platform from config_flow.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + if wiz_data.bulb.bulbtype.bulb_type != BulbClass.SOCKET: + async_add_entities([WizBulbEntity(wiz_data, entry.title)]) + + +class WizBulbEntity(WizToggleEntity, LightEntity): + """Representation of WiZ Light bulb.""" + + def __init__(self, wiz_data: WizData, name: str) -> None: + """Initialize an WiZLight.""" + super().__init__(wiz_data, name) + bulb_type: BulbType = self._device.bulbtype + features: Features = bulb_type.features + color_modes = set() + if features.color: + color_modes.add(RGB_WHITE_CHANNELS_COLOR_MODE[bulb_type.white_channels]) + if features.color_tmp: + color_modes.add(COLOR_MODE_COLOR_TEMP) + if not color_modes and features.brightness: + color_modes.add(COLOR_MODE_BRIGHTNESS) + self._attr_supported_color_modes = color_modes + self._attr_effect_list = wiz_data.scenes + if bulb_type.bulb_type != BulbClass.DW: + kelvin = bulb_type.kelvin_range + self._attr_min_mireds = color_temperature_kelvin_to_mired(kelvin.max) + self._attr_max_mireds = color_temperature_kelvin_to_mired(kelvin.min) + if bulb_type.features.effect: + self._attr_supported_features = SUPPORT_EFFECT + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + state = self._device.state + color_modes = self.supported_color_modes + assert color_modes is not None + if (brightness := state.get_brightness()) is not None: + self._attr_brightness = max(0, min(255, brightness)) + if COLOR_MODE_COLOR_TEMP in color_modes and ( + color_temp := state.get_colortemp() + ): + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + self._attr_color_temp = color_temperature_kelvin_to_mired(color_temp) + elif ( + COLOR_MODE_RGBWW in color_modes and (rgbww := state.get_rgbww()) is not None + ): + self._attr_rgbww_color = rgbww + self._attr_color_mode = COLOR_MODE_RGBWW + elif COLOR_MODE_RGBW in color_modes and (rgbw := state.get_rgbw()) is not None: + self._attr_rgbw_color = rgbw + self._attr_color_mode = COLOR_MODE_RGBW + else: + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + self._attr_effect = state.get_scene() + super()._async_update_attrs() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + await self._device.turn_on(_async_pilot_builder(**kwargs)) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wiz/manifest.json b/homeassistant/components/wiz/manifest.json new file mode 100644 index 0000000000000..26d32589c94b9 --- /dev/null +++ b/homeassistant/components/wiz/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "wiz", + "name": "WiZ", + "config_flow": true, + "dhcp": [ + {"registered_devices": true}, + {"macaddress":"A8BB50*"}, + {"macaddress":"D8A011*"}, + {"macaddress":"444F8E*"}, + {"macaddress":"6C2990*"}, + {"hostname":"wiz_*"} + ], + "dependencies": ["network"], + "quality_scale": "platinum", + "documentation": "https://www.home-assistant.io/integrations/wiz", + "requirements": ["pywizlight==0.5.13"], + "iot_class": "local_push", + "codeowners": ["@sbidy"] +} diff --git a/homeassistant/components/wiz/models.py b/homeassistant/components/wiz/models.py new file mode 100644 index 0000000000000..efbb2a664b152 --- /dev/null +++ b/homeassistant/components/wiz/models.py @@ -0,0 +1,15 @@ +"""WiZ integration models.""" +from dataclasses import dataclass + +from pywizlight import wizlight + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class WizData: + """Data for the wiz integration.""" + + coordinator: DataUpdateCoordinator + bulb: wizlight + scenes: list diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py new file mode 100644 index 0000000000000..f7d827534b387 --- /dev/null +++ b/homeassistant/components/wiz/number.py @@ -0,0 +1,121 @@ +"""Support for WiZ effect speed numbers.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Optional, cast + +from pywizlight import wizlight + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import WizEntity +from .models import WizData + + +@dataclass +class WizNumberEntityDescriptionMixin: + """Mixin to describe a WiZ number entity.""" + + value_fn: Callable[[wizlight], int | None] + set_value_fn: Callable[[wizlight, int], Coroutine[None, None, None]] + required_feature: str + + +@dataclass +class WizNumberEntityDescription( + NumberEntityDescription, WizNumberEntityDescriptionMixin +): + """Class to describe a WiZ number entity.""" + + +async def _async_set_speed(device: wizlight, speed: int) -> None: + await device.set_speed(speed) + + +async def _async_set_ratio(device: wizlight, ratio: int) -> None: + await device.set_ratio(ratio) + + +NUMBERS: tuple[WizNumberEntityDescription, ...] = ( + WizNumberEntityDescription( + key="effect_speed", + min_value=10, + max_value=200, + step=1, + icon="mdi:speedometer", + name="Effect Speed", + value_fn=lambda device: cast(Optional[int], device.state.get_speed()), + set_value_fn=_async_set_speed, + required_feature="effect", + ), + WizNumberEntityDescription( + key="dual_head_ratio", + min_value=0, + max_value=100, + step=1, + icon="mdi:floor-lamp-dual", + name="Dual Head Ratio", + value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), + set_value_fn=_async_set_ratio, + required_feature="dual_head", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the wiz speed number.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + WizSpeedNumber(wiz_data, entry.title, description) + for description in NUMBERS + if getattr(wiz_data.bulb.bulbtype.features, description.required_feature) + ) + + +class WizSpeedNumber(WizEntity, NumberEntity): + """Defines a WiZ speed number.""" + + entity_description: WizNumberEntityDescription + _attr_mode = NumberMode.SLIDER + + def __init__( + self, wiz_data: WizData, name: str, description: WizNumberEntityDescription + ) -> None: + """Initialize an WiZ device.""" + super().__init__(wiz_data, name) + self.entity_description = description + self._attr_unique_id = f"{self._device.mac}_{description.key}" + self._attr_name = f"{name} {description.name}" + self._async_update_attrs() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.entity_description.value_fn(self._device) is not None + ) + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + if (value := self.entity_description.value_fn(self._device)) is not None: + self._attr_value = float(value) + + async def async_set_value(self, value: float) -> None: + """Set the speed value.""" + await self.entity_description.set_value_fn(self._device, int(value)) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wiz/strings.json b/homeassistant/components/wiz/strings.json new file mode 100644 index 0000000000000..d9b2a19d752e1 --- /dev/null +++ b/homeassistant/components/wiz/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::ip%]" + }, + "description": "If you leave the IP Address empty, discovery will be used to find devices." + }, + "discovery_confirm": { + "description": "Do you want to setup {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Device" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "bulb_time_out": "Can not connect to the bulb. Maybe the bulb is offline or a wrong IP was entered. Please turn on the light and try again!", + "no_wiz_light": "The bulb can not be connected via WiZ Platform integration.", + "no_ip": "Not a valid IP address." + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/switch.py b/homeassistant/components/wiz/switch.py new file mode 100644 index 0000000000000..ffe75910b40af --- /dev/null +++ b/homeassistant/components/wiz/switch.py @@ -0,0 +1,41 @@ +"""WiZ integration switch platform.""" +from __future__ import annotations + +from typing import Any + +from pywizlight import PilotBuilder +from pywizlight.bulblibrary import BulbClass + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import WizToggleEntity +from .models import WizData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the WiZ switch platform.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + if wiz_data.bulb.bulbtype.bulb_type == BulbClass.SOCKET: + async_add_entities([WizSocketEntity(wiz_data, entry.title)]) + + +class WizSocketEntity(WizToggleEntity, SwitchEntity): + """Representation of a WiZ socket.""" + + def __init__(self, wiz_data: WizData, name: str) -> None: + """Initialize a WiZ socket.""" + super().__init__(wiz_data, name) + self._async_update_attrs() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the socket to turn on.""" + await self._device.turn_on(PilotBuilder()) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wiz/translations/bg.json b/homeassistant/components/wiz/translations/bg.json new file mode 100644 index 0000000000000..eb0697ca13d6b --- /dev/null +++ b/homeassistant/components/wiz/translations/bg.json @@ -0,0 +1,29 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/ca.json b/homeassistant/components/wiz/translations/ca.json new file mode 100644 index 0000000000000..60ca2a8a884ad --- /dev/null +++ b/homeassistant/components/wiz/translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "bulb_time_out": "No s'ha pogut connectar amb la bombeta. Pot ser que la bombeta estigui desconnectada o s'hagi introdu\u00eft una IP incorrecta. Enc\u00e9n el llum i torna-ho a provar.", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_ip": "Adre\u00e7a IP no v\u00e0lida.", + "no_wiz_light": "La bombeta no es pot connectar mitjan\u00e7ant la integraci\u00f3 Plataforma WiZ.", + "unknown": "Error inesperat" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "discovery_confirm": { + "description": "Vols configurar {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositiu" + } + }, + "user": { + "data": { + "host": "Adre\u00e7a IP", + "name": "Nom" + }, + "description": "Si deixes l'adre\u00e7a IP buida, s'utilitzar\u00e0 el descobriment per cercar dispositius." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/cs.json b/homeassistant/components/wiz/translations/cs.json new file mode 100644 index 0000000000000..0656e47c8bef0 --- /dev/null +++ b/homeassistant/components/wiz/translations/cs.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "host": "IP adresa", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/de.json b/homeassistant/components/wiz/translations/de.json new file mode 100644 index 0000000000000..73b933fefcf90 --- /dev/null +++ b/homeassistant/components/wiz/translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "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.", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {name} ({host}) einrichten?" + }, + "pick_device": { + "data": { + "device": "Ger\u00e4t" + } + }, + "user": { + "data": { + "host": "IP-Adresse", + "name": "Name" + }, + "description": "Wenn du die IP-Adresse leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/el.json b/homeassistant/components/wiz/translations/el.json new file mode 100644 index 0000000000000..8b0f37864c570 --- /dev/null +++ b/homeassistant/components/wiz/translations/el.json @@ -0,0 +1,37 @@ +{ + "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", + "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": { + "bulb_time_out": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1. \u038a\u03c3\u03c9\u03c2 \u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03bb\u03ac\u03b8\u03bf\u03c2 IP/host. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03c6\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac!", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_ip": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP.", + "no_wiz_light": "\u039f \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 WiZ.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} ({host})", + "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;" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" + }, + "pick_device": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + }, + "user": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \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 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03bd\u03ad\u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/en.json b/homeassistant/components/wiz/translations/en.json new file mode 100644 index 0000000000000..a612ea165a422 --- /dev/null +++ b/homeassistant/components/wiz/translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "no_devices_found": "No devices found on the network" + }, + "error": { + "bulb_time_out": "Can not connect to the bulb. Maybe the bulb is offline or a wrong IP was entered. Please turn on the light and try again!", + "cannot_connect": "Failed to connect", + "no_ip": "Not a valid IP address.", + "no_wiz_light": "The bulb can not be connected via WiZ Platform integration.", + "unknown": "Unexpected error" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "discovery_confirm": { + "description": "Do you want to setup {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "user": { + "data": { + "host": "IP Address", + "name": "Name" + }, + "description": "If you leave the IP Address empty, discovery will be used to find devices." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json new file mode 100644 index 0000000000000..bc06bff80535b --- /dev/null +++ b/homeassistant/components/wiz/translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo ya configurado", + "cannot_connect": "Error al conectar", + "no_devices_found": "Ning\u00fan dispositivo encontrado en la red." + }, + "error": { + "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ingres\u00f3 una IP incorrecta. \u00a1Por favor encienda la luz y vuelve a intentarlo!", + "cannot_connect": "Error al conectar", + "no_ip": "No es una direcci\u00f3n IP v\u00e1lida.", + "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n de WiZ Platform.", + "unknown": "Error inesperado" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "\u00bfDesea iniciar la configuraci\u00f3n?" + }, + "discovery_confirm": { + "description": "\u00bfDesea configurar {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Si deja la direcci\u00f3n IP vac\u00eda, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/et.json b/homeassistant/components/wiz/translations/et.json new file mode 100644 index 0000000000000..9cb84a0e7bd74 --- /dev/null +++ b/homeassistant/components/wiz/translations/et.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud" + }, + "error": { + "bulb_time_out": "Ei saa \u00fchendust pirniga. Pirn v\u00f5ib-olla v\u00f5rgu\u00fchenduseta v\u00f5i on sisestatud vale IP aadress. L\u00fclita lamp sisse ja proovi uuesti!", + "cannot_connect": "\u00dchendamine nurjus", + "no_ip": "Sobimatu IP-aadress", + "no_wiz_light": "Pirni ei saa \u00fchendada WiZ Platvormi sidumise kaudu.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Kas alustan seadistamist?" + }, + "discovery_confirm": { + "description": "Kas soovid seadistada {name}({host})?" + }, + "pick_device": { + "data": { + "device": "Seade" + } + }, + "user": { + "data": { + "host": "IP aadress", + "name": "Nimi" + }, + "description": "Kui j\u00e4tad IP aadressi t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json new file mode 100644 index 0000000000000..e6123a1d7159b --- /dev/null +++ b/homeassistant/components/wiz/translations/fr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "error": { + "bulb_time_out": "Impossible de se connecter \u00e0 l'ampoule. Peut-\u00eatre que l'ampoule est hors ligne ou qu'une mauvaise adresse IP a \u00e9t\u00e9 saisie. Veuillez allumer la lumi\u00e8re et r\u00e9essayer\u00a0!", + "cannot_connect": "\u00c9chec de connexion", + "no_ip": "Adresse IP non valide", + "no_wiz_light": "L'ampoule ne peut pas \u00eatre connect\u00e9e via l'int\u00e9gration de la plate-forme WiZ.", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration ?" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {name} ({host}) ?" + }, + "pick_device": { + "data": { + "device": "Appareil" + } + }, + "user": { + "data": { + "host": "Adresse IP", + "name": "Nom" + }, + "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/he.json b/homeassistant/components/wiz/translations/he.json new file mode 100644 index 0000000000000..8d4e41401c80a --- /dev/null +++ b/homeassistant/components/wiz/translations/he.json @@ -0,0 +1,33 @@ +{ + "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", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "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" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "discovery_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + }, + "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json new file mode 100644 index 0000000000000..7c99571a9ae14 --- /dev/null +++ b/homeassistant/components/wiz/translations/hu.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rj\u00fck, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm.", + "no_wiz_light": "Az izz\u00f3 nem csatlakoztathat\u00f3 a WiZ Platform integr\u00e1ci\u00f3n kereszt\u00fcl.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "IP c\u00edm", + "name": "N\u00e9v" + }, + "description": "Ha az IP-c\u00edmet \u00fcresen hagyja, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/id.json b/homeassistant/components/wiz/translations/id.json new file mode 100644 index 0000000000000..694973f8ffab0 --- /dev/null +++ b/homeassistant/components/wiz/translations/id.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "bulb_time_out": "Tidak dapat terhubung ke bohlam. Mungkin bohlam offline atau IP yang salah dimasukkan. Nyalakan lampu dan coba lagi!", + "cannot_connect": "Gagal terhubung", + "no_ip": "Bukan alamat IP yang valid.", + "no_wiz_light": "Bohlam tidak dapat dihubungkan melalui integrasi Platform WiZ.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, + "user": { + "data": { + "host": "Alamat IP", + "name": "Nama" + }, + "description": "Jika Alamat IP dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/it.json b/homeassistant/components/wiz/translations/it.json new file mode 100644 index 0000000000000..ebd093c22f7e9 --- /dev/null +++ b/homeassistant/components/wiz/translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "bulb_time_out": "Impossibile connettersi alla lampadina. Forse la lampadina non \u00e8 in linea o \u00e8 stato inserito un IP errato. AccendI la luce e riprova!", + "cannot_connect": "Impossibile connettersi", + "no_ip": "Non \u00e8 un indirizzo IP valido.", + "no_wiz_light": "La lampadina non pu\u00f2 essere collegata tramite l'integrazione della piattaforma WiZ.", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "discovery_confirm": { + "description": "Vuoi configurare {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Indirizzo IP", + "name": "Nome" + }, + "description": "Se lasci vuoto l'indirizzo IP, il rilevamento sar\u00e0 utilizzato per trovare i dispositivi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json new file mode 100644 index 0000000000000..8d9cbd0c05522 --- /dev/null +++ b/homeassistant/components/wiz/translations/ja.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u9593\u9055\u3063\u305fIP/\u30db\u30b9\u30c8\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u96fb\u7403\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_ip": "\u6709\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, + "discovery_confirm": { + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d" + }, + "description": "\u65b0\u3057\u3044\u96fb\u7403(bulb)\u3092\u8ffd\u52a0\u3059\u308b\u306b\u306f\u3001\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3068\u540d\u524d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/lv.json b/homeassistant/components/wiz/translations/lv.json new file mode 100644 index 0000000000000..dcf6c75a65372 --- /dev/null +++ b/homeassistant/components/wiz/translations/lv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "Ier\u012bce" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/nl.json b/homeassistant/components/wiz/translations/nl.json new file mode 100644 index 0000000000000..79fc5c3db1f72 --- /dev/null +++ b/homeassistant/components/wiz/translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "bulb_time_out": "Kan geen verbinding maken met de lamp. Misschien is de lamp offline of is er een verkeerde IP/host ingevoerd. Doe het licht aan en probeer het opnieuw!", + "cannot_connect": "Kan geen verbinding maken", + "no_ip": "Geen geldig IP-adres.", + "no_wiz_light": "De lamp kan niet worden aangesloten via WiZ Platform integratie.", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "discovery_confirm": { + "description": "Wilt u {name} ({host}) instellen?" + }, + "pick_device": { + "data": { + "device": "Apparaat" + } + }, + "user": { + "data": { + "host": "IP-adres", + "name": "Naam" + }, + "description": "Als u het IP-adres leeg laat, zal discovery worden gebruikt om apparaten te vinden." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/no.json b/homeassistant/components/wiz/translations/no.json new file mode 100644 index 0000000000000..7f2090b5b71fe --- /dev/null +++ b/homeassistant/components/wiz/translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "bulb_time_out": "Kan ikke koble til p\u00e6ren. Kanskje p\u00e6ren er frakoblet eller feil IP ble lagt inn. Sl\u00e5 p\u00e5 lyset, og pr\u00f8v p\u00e5 nytt!", + "cannot_connect": "Tilkobling mislyktes", + "no_ip": "Ikke en gyldig IP-adresse.", + "no_wiz_light": "P\u00e6ren kan ikke kobles til via WiZ Platform-integrasjon.", + "unknown": "Uventet feil" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "discovery_confirm": { + "description": "Vil du konfigurere {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "IP adresse", + "name": "Navn" + }, + "description": "Hvis du lar IP-adressen st\u00e5 tom, vil oppdagelse bli brukt til \u00e5 finne enheter." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/pl.json b/homeassistant/components/wiz/translations/pl.json new file mode 100644 index 0000000000000..30d1455c470a2 --- /dev/null +++ b/homeassistant/components/wiz/translations/pl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "bulb_time_out": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z \u017car\u00f3wk\u0105. Mo\u017ce \u017car\u00f3wka jest w trybie offline lub wprowadzono z\u0142y adres IP. W\u0142\u0105cz \u015bwiat\u0142o i spr\u00f3buj ponownie!", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_ip": "Nieprawid\u0142owy adres IP", + "no_wiz_light": "\u017bar\u00f3wka nie mo\u017ce by\u0107 pod\u0142\u0105czona poprzez integracj\u0119 z platform\u0105 WiZ.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } + }, + "user": { + "data": { + "host": "Adres IP", + "name": "Nazwa" + }, + "description": "Je\u015bli nie podasz adresu IP, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/pt-BR.json b/homeassistant/components/wiz/translations/pt-BR.json new file mode 100644 index 0000000000000..ce27ea82abc02 --- /dev/null +++ b/homeassistant/components/wiz/translations/pt-BR.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "bulb_time_out": "N\u00e3o foi poss\u00edvel se conectar \u00e0 l\u00e2mpada. Talvez a l\u00e2mpada esteja offline ou um IP errado foi inserido. Por favor, acenda a luz e tente novamente!", + "cannot_connect": "Falha ao conectar", + "no_ip": "N\u00e3o \u00e9 um endere\u00e7o IP v\u00e1lido.", + "no_wiz_light": "A l\u00e2mpada n\u00e3o pode ser conectada via integra\u00e7\u00e3o com a plataforma WiZ.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "discovery_confirm": { + "description": "Deseja configurar {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Endere\u00e7o IP", + "name": "Nome" + }, + "description": "Se voc\u00ea deixar o endere\u00e7o IP vazio, a descoberta ser\u00e1 usada para localizar dispositivos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/ru.json b/homeassistant/components/wiz/translations/ru.json new file mode 100644 index 0000000000000..dd422f29fea02 --- /dev/null +++ b/homeassistant/components/wiz/translations/ru.json @@ -0,0 +1,37 @@ +{ + "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.", + "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": { + "bulb_time_out": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043b\u0430\u043c\u043f\u043e\u0447\u043a\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435, \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043b\u0438 \u043b\u0430\u043c\u043f\u043e\u0447\u043a\u0430 \u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043b\u0438 \u0443\u043a\u0430\u0437\u0430\u043d IP-\u0430\u0434\u0440\u0435\u0441.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_ip": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "no_wiz_light": "\u042d\u0442\u0443 \u043b\u0430\u043c\u043f\u043e\u0447\u043a\u0443 \u043d\u0435\u043b\u044c\u0437\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0447\u0435\u0440\u0435\u0437 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e WiZ.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/sk.json b/homeassistant/components/wiz/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/wiz/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/tr.json b/homeassistant/components/wiz/translations/tr.json new file mode 100644 index 0000000000000..3f6b1f68dc5ec --- /dev/null +++ b/homeassistant/components/wiz/translations/tr.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "bulb_time_out": "Ampul ba\u011flanam\u0131yor. Belki ampul \u00e7evrimd\u0131\u015f\u0131d\u0131r veya yanl\u0131\u015f bir IP girilmi\u015ftir. L\u00fctfen \u0131\u015f\u0131\u011f\u0131 a\u00e7\u0131n ve tekrar deneyin!", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_ip": "Ge\u00e7erli bir IP adresi de\u011fil.", + "no_wiz_light": "Ampul WiZ Platform entegrasyonu ile ba\u011flanamaz.", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "discovery_confirm": { + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + }, + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "IP Adresi", + "name": "Ad" + }, + "description": "IP Adresini bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/zh-Hant.json b/homeassistant/components/wiz/translations/zh-Hant.json new file mode 100644 index 0000000000000..b677427996fbe --- /dev/null +++ b/homeassistant/components/wiz/translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "error": { + "bulb_time_out": "\u7121\u6cd5\u9023\u7dda\u81f3\u71c8\u6ce1\u3002\u53ef\u80fd\u539f\u56e0\u70ba\u71c8\u6ce1\u5df2\u96e2\u7dda\u6216\u6240\u8f38\u5165\u7684 IP \u932f\u8aa4\u3002\u8acb\u958b\u555f\u71c8\u6ce1\u4e26\u518d\u8a66\u4e00\u6b21\uff01", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_ip": "\u975e\u6709\u6548 IP \u4f4d\u5740\u3002", + "no_wiz_light": "\u71c8\u6ce1\u7121\u6cd5\u900f\u904e WiZ \u5e73\u53f0\u6574\u5408\u9023\u63a5\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name} ({host})", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" + }, + "pick_device": { + "data": { + "device": "\u88dd\u7f6e" + } + }, + "user": { + "data": { + "host": "IP \u4f4d\u5740", + "name": "\u540d\u7a31" + }, + "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/utils.py b/homeassistant/components/wiz/utils.py new file mode 100644 index 0000000000000..278989b5b2b99 --- /dev/null +++ b/homeassistant/components/wiz/utils.py @@ -0,0 +1,24 @@ +"""WiZ utils.""" +from __future__ import annotations + +from pywizlight import BulbType +from pywizlight.bulblibrary import BulbClass + +from .const import DEFAULT_NAME + + +def _short_mac(mac: str) -> str: + """Get the short mac address from the full mac.""" + return mac.replace(":", "").upper()[-6:] + + +def name_from_bulb_type_and_mac(bulb_type: BulbType, mac: str) -> str: + """Generate a name from bulb_type and mac.""" + if bulb_type.bulb_type == BulbClass.RGB: + if bulb_type.white_channels == 2: + description = "RGBWW Tunable" + else: + description = "RGBW Tunable" + else: + description = bulb_type.bulb_type.value + return f"{DEFAULT_NAME} {description} {_short_mac(mac)}" diff --git a/homeassistant/components/wled/translations/bg.json b/homeassistant/components/wled/translations/bg.json index a511aad9e4289..b023b5d601131 100644 --- a/homeassistant/components/wled/translations/bg.json +++ b/homeassistant/components/wled/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cct_unsupported": "\u0422\u043e\u0432\u0430 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 CCT \u043a\u0430\u043d\u0430\u043b\u0438, \u043a\u043e\u0438\u0442\u043e \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" }, "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/wled/translations/el.json b/homeassistant/components/wled/translations/el.json index d931bc69d70ea..d6c3dbbc83798 100644 --- a/homeassistant/components/wled/translations/el.json +++ b/homeassistant/components/wled/translations/el.json @@ -1,14 +1,19 @@ { "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", "cct_unsupported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae WLED \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9\u03b1 CCT, \u03c4\u03b1 \u03bf\u03c0\u03bf\u03af\u03b1 \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" }, "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": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf WLED \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Home Assistant." }, "zeroconf_confirm": { @@ -16,5 +21,14 @@ "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae WLED" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "\u0394\u03b9\u03b1\u03c4\u03b7\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03cd\u03c1\u03b9\u03bf \u03c6\u03c9\u03c2, \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ba\u03b1\u03b9 \u03bc\u03b5 1 \u03c4\u03bc\u03ae\u03bc\u03b1 LED." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json index 621b11e4af59c..f52d88f5401bb 100644 --- a/homeassistant/components/wled/translations/id.json +++ b/homeassistant/components/wled/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "cct_unsupported": "Perangkat WLED ini menggunakan saluran CCT, yang tidak didukung oleh integrasi ini" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 0efbe56e3e0e2..5f30617d7eb43 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 8423f2d3f48d6..d1ba5b75ec33d 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "cct_unsupported": "Dit WLED-apparaat maakt gebruik van CCT-kanalen, wat niet wordt ondersteund door deze integratie" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/wled/translations/pt-BR.json b/homeassistant/components/wled/translations/pt-BR.json new file mode 100644 index 0000000000000..c922f86d776df --- /dev/null +++ b/homeassistant/components/wled/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "cct_unsupported": "Este dispositivo WLED usa canais CCT, que n\u00e3o s\u00e3o suportados por esta integra\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure seu WLED para integra\u00e7\u00e3o com o Home Assistant." + }, + "zeroconf_confirm": { + "description": "Deseja adicionar o WLED chamado `{name}` ao Home Assistant?", + "title": "Dispositivo WLED descoberto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Mantenha a luz principal, mesmo com 1 segmento de LED." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.el.json b/homeassistant/components/wled/translations/select.el.json new file mode 100644 index 0000000000000..ee65d6e5c8cc1 --- /dev/null +++ b/homeassistant/components/wled/translations/select.el.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "1": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "2": "\u039c\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.pt-BR.json b/homeassistant/components/wled/translations/select.pt-BR.json new file mode 100644 index 0000000000000..0323f9f7960d1 --- /dev/null +++ b/homeassistant/components/wled/translations/select.pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "wled__live_override": { + "0": "Desligado", + "1": "Ligado", + "2": "At\u00e9 que o dispositivo seja reiniciado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json index aea858c5bfcbb..a795bd523591f 100644 --- a/homeassistant/components/wled/translations/sv.json +++ b/homeassistant/components/wled/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten har redan konfigurerats" + "already_configured": "Enheten har redan konfigurerats", + "cct_unsupported": "Denna WLED-enhet anv\u00e4nder CCT-kanaler, vilket inte st\u00f6ds av denna integration" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/wolflink/manifest.json b/homeassistant/components/wolflink/manifest.json index 749f7bbc67c1f..f597093382ee0 100644 --- a/homeassistant/components/wolflink/manifest.json +++ b/homeassistant/components/wolflink/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/wolflink", "requirements": ["wolf_smartset==0.1.11"], "codeowners": ["@adamkrol93"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["wolf_smartset"] } diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index f1a94cbbe20e9..a39b03fbd9f9a 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -152,10 +152,11 @@ def device_class(self): def native_value(self): """Return the state converting with supported values.""" state = super().native_value - resolved_state = [ - item for item in self.wolf_object.items if item.value == int(state) - ] - if resolved_state: - resolved_name = resolved_state[0].name - return STATES.get(resolved_name, resolved_name) + if state is not None: + resolved_state = [ + item for item in self.wolf_object.items if item.value == int(state) + ] + if resolved_state: + resolved_name = resolved_state[0].name + return STATES.get(resolved_name, resolved_name) return state diff --git a/homeassistant/components/wolflink/translations/el.json b/homeassistant/components/wolflink/translations/el.json new file mode 100644 index 0000000000000..cd3c573a4dd82 --- /dev/null +++ b/homeassistant/components/wolflink/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" + }, + "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": { + "device": { + "data": { + "device_name": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 WOLF" + }, + "user": { + "data": { + "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" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 WOLF SmartSet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/pt-BR.json b/homeassistant/components/wolflink/translations/pt-BR.json index 43e2720b36511..617a2398ae813 100644 --- a/homeassistant/components/wolflink/translations/pt-BR.json +++ b/homeassistant/components/wolflink/translations/pt-BR.json @@ -1,19 +1,26 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "cannot_connect": "Falha na conex\u00e3o" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "device": { "data": { "device_name": "Dispositivo" - } + }, + "title": "Selecione o dispositivo WOLF" }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" - } + }, + "title": "Conex\u00e3o WOLF SmartSet" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.el.json b/homeassistant/components/wolflink/translations/sensor.el.json index 75ab523afd2a8..4b7813b0c7405 100644 --- a/homeassistant/components/wolflink/translations/sensor.el.json +++ b/homeassistant/components/wolflink/translations/sensor.el.json @@ -1,15 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "\u0391\u03c0\u03bf\u03c3\u03b2\u03b5\u03c3\u03c4\u03ae\u03c1\u03b1\u03c2 \u03ba\u03b1\u03c5\u03c3\u03b1\u03b5\u03c1\u03af\u03c9\u03bd", + "absenkbetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03c0\u03b9\u03c3\u03b8\u03bf\u03b4\u03c1\u03cc\u03bc\u03b7\u03c3\u03b7\u03c2", + "absenkstop": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03bf\u03c0\u03b9\u03c3\u03b8\u03bf\u03b4\u03c1\u03cc\u03bc\u03b7\u03c3\u03b7\u03c2", + "aktiviert": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "antilegionellenfunktion": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03c2 \u03bb\u03b5\u03b3\u03b9\u03bf\u03bd\u03ad\u03bb\u03bb\u03b1\u03c2", + "at_abschaltung": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 OT", + "at_frostschutz": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1 \u03b1\u03c0\u03cc \u03c0\u03b1\u03b3\u03b5\u03c4\u03cc OT", + "aus": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf OFF", + "automatik_ein": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf ON", + "bereit_keine_ladung": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf, \u03b4\u03b5\u03bd \u03c6\u03bf\u03c1\u03c4\u03ce\u03bd\u03b5\u03b9", + "betrieb_ohne_brenner": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03b1\u03c5\u03c3\u03c4\u03ae\u03c1\u03b1", + "cooling": "\u03a8\u03cd\u03be\u03b7", + "deaktiviert": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", + "dhw_prior": "DHWPrior", + "eco": "Eco", + "ein": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "estrichtrocknung": "\u03a3\u03c4\u03ad\u03b3\u03bd\u03c9\u03bc\u03b1 \u03b5\u03c0\u03af\u03c3\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2", "externe_deaktivierung": "\u0395\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "fernschalter_ein": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u0395\u03bd\u03b5\u03c1\u03b3\u03ae", "frost_heizkreis": "\u03a0\u03b1\u03b3\u03b5\u03c4\u03cc\u03c2 \u03c3\u03c4\u03bf \u03ba\u03cd\u03ba\u03bb\u03c9\u03bc\u03b1 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", + "frost_warmwasser": "\u03a0\u03b1\u03b3\u03b5\u03c4\u03cc\u03c2 DHW", "frostschutz": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1 \u03b1\u03c0\u03cc \u03c0\u03b1\u03b3\u03b5\u03c4\u03cc", "gasdruck": "\u03a0\u03af\u03b5\u03c3\u03b7 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5", + "glt_betrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 BMS", "gradienten_uberwachung": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03ba\u03bb\u03af\u03c3\u03b7\u03c2", "heizbetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", "heizgerat_mit_speicher": "\u039b\u03ad\u03b2\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03ba\u03cd\u03bb\u03b9\u03bd\u03b4\u03c1\u03bf", - "heizung": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7" + "heizung": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7", + "initialisierung": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "kalibration": "\u0392\u03b1\u03b8\u03bc\u03bf\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7", + "kalibration_heizbetrieb": "\u0392\u03b1\u03b8\u03bc\u03bf\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", + "kalibration_kombibetrieb": "\u0392\u03b1\u03b8\u03bc\u03bf\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 Combi", + "kalibration_warmwasserbetrieb": "\u0392\u03b1\u03b8\u03bc\u03bf\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 DHW", + "kaskadenbetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03c1\u03ac\u03ba\u03c4\u03b7", + "kombibetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Combi", + "kombigerat": "\u039c\u03c0\u03cc\u03b9\u03bb\u03b5\u03c1 Combi", + "kombigerat_mit_solareinbindung": "\u039c\u03c0\u03cc\u03b9\u03bb\u03b5\u03c1 Combi \u03bc\u03b5 \u03b7\u03bb\u03b9\u03b1\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7", + "mindest_kombizeit": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03c3\u03c5\u03bd\u03b4\u03c5\u03b1\u03c3\u03bc\u03bf\u03cd", + "nachlauf_heizkreispumpe": "\u0391\u03bd\u03c4\u03bb\u03af\u03b1 \u03ba\u03c5\u03ba\u03bb\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "nachspulen": "\u039c\u03b5\u03c4\u03ac \u03c4\u03bf \u03be\u03ad\u03c0\u03bb\u03c5\u03bc\u03b1", + "nur_heizgerat": "\u039c\u03cc\u03bd\u03bf \u03bb\u03ad\u03b2\u03b7\u03c4\u03b1\u03c2", + "parallelbetrieb": "\u03a0\u03b1\u03c1\u03ac\u03bb\u03bb\u03b7\u03bb\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "partymodus": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03ac\u03c1\u03c4\u03b9", + "perm_cooling": "PermCooling", + "permanent": "\u039c\u03cc\u03bd\u03b9\u03bc\u03bf", + "permanentbetrieb": "\u039c\u03cc\u03bd\u03b9\u03bc\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "reduzierter_betrieb": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "rt_abschaltung": "\u03a4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03cc\u03c2 RT", + "rt_frostschutz": "RT \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1 \u03b1\u03c0\u03cc \u03c0\u03b1\u03b3\u03b5\u03c4\u03cc", + "ruhekontakt": "\u0395\u03c0\u03b1\u03c6\u03ae \u03b1\u03bd\u03ac\u03c0\u03b1\u03c5\u03c3\u03b7\u03c2", + "schornsteinfeger": "\u0394\u03bf\u03ba\u03b9\u03bc\u03ae \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ce\u03bd", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "softstart": "\u0389\u03c0\u03b9\u03b1 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "solarbetrieb": "\u0397\u03bb\u03b9\u03b1\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", + "sparbetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03b9\u03ba\u03bf\u03bd\u03bf\u03bc\u03af\u03b1\u03c2", + "sparen": "\u039f\u03b9\u03ba\u03bf\u03bd\u03bf\u03bc\u03af\u03b1", + "spreizung_hoch": "dT \u03c0\u03bf\u03bb\u03cd \u03c6\u03b1\u03c1\u03b4\u03cd", + "spreizung_kf": "\u0394\u03b9\u03ac\u03b4\u03bf\u03c3\u03b7 KF", + "stabilisierung": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "standby": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2", + "start": "\u0388\u03bd\u03b1\u03c1\u03be\u03b7", + "storung": "\u0392\u03bb\u03ac\u03b2\u03b7", + "taktsperre": "\u0391\u03bd\u03c4\u03b9-\u03ba\u03cd\u03ba\u03bb\u03bf\u03c2", + "telefonfernschalter": "\u03a4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03b9\u03ba\u03cc\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2", + "test": "\u0394\u03bf\u03ba\u03b9\u03bc\u03ae", + "tpw": "TPW", + "urlaubsmodus": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd", + "ventilprufung": "\u0394\u03bf\u03ba\u03b9\u03bc\u03ae \u03b2\u03b1\u03bb\u03b2\u03af\u03b4\u03b1\u03c2", + "vorspulen": "\u039e\u03ad\u03b2\u03b3\u03b1\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "warmwasser": "DHW", + "warmwasser_schnellstart": "\u0393\u03c1\u03ae\u03b3\u03bf\u03c1\u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 DHW", + "warmwasserbetrieb": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 DHW", + "warmwassernachlauf": "\u0395\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 DHW", + "warmwasservorrang": "\u03a0\u03c1\u03bf\u03c4\u03b5\u03c1\u03b1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 DHW", + "zunden": "\u0391\u03bd\u03ac\u03c6\u03bb\u03b5\u03be\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index 9e7932b2dd74f..3ceb09bf90a9c 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -57,6 +57,11 @@ "test": "Pengujian", "urlaubsmodus": "Mode liburan", "ventilprufung": "Uji katup", + "warmwasser": "Air Panas Domestik", + "warmwasser_schnellstart": "Mulai cepat Air Panas Domestik", + "warmwasserbetrieb": "Mode Air Panas Domestik", + "warmwassernachlauf": "Air Panas Domestik sisa", + "warmwasservorrang": "Prioritas Air Panas Domestik", "zunden": "Pengapian" } } diff --git a/homeassistant/components/wolflink/translations/sensor.pt-BR.json b/homeassistant/components/wolflink/translations/sensor.pt-BR.json index 2af363e8f1c0b..7c2cdca7bf8fa 100644 --- a/homeassistant/components/wolflink/translations/sensor.pt-BR.json +++ b/homeassistant/components/wolflink/translations/sensor.pt-BR.json @@ -1,18 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Amortecedor de g\u00e1s de combust\u00e3o", + "absenkbetrieb": "Modo de recuo", + "absenkstop": "Parada de recuo", "aktiviert": "Ativado", + "antilegionellenfunktion": "Fun\u00e7\u00e3o anti-legionela", + "at_abschaltung": "Desligamento OT", + "at_frostschutz": "Prote\u00e7\u00e3o contra geada OT", "aus": "Desativado", + "auto": "Auto", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Automatic OFF", + "automatik_ein": "Automatic ON", + "bereit_keine_ladung": "Pronto, n\u00e3o carregando", + "betrieb_ohne_brenner": "Trabalhando sem bico", + "cooling": "Resfriamento", "deaktiviert": "Inativo", + "dhw_prior": "DHWPrior", "eco": "Econ\u00f4mico", "ein": "Habilitado", + "estrichtrocknung": "Secagem da mesa", + "externe_deaktivierung": "Desativa\u00e7\u00e3o externa", + "fernschalter_ein": "Controle remoto ativado", + "frost_heizkreis": "Congelamento do circuito de aquecimento", + "frost_warmwasser": "Geada DHW", + "frostschutz": "Prote\u00e7\u00e3o contra geada", + "gasdruck": "Press\u00e3o do g\u00e1s", + "glt_betrieb": "Modo BMS", + "gradienten_uberwachung": "Monitoramento de gradiente", + "heizbetrieb": "Modo de aquecimento", + "heizgerat_mit_speicher": "Boiler com cilindro", + "heizung": "Aquecimento", + "initialisierung": "Inicializa\u00e7\u00e3o", + "kalibration": "Calibra\u00e7\u00e3o", + "kalibration_heizbetrieb": "Calibra\u00e7\u00e3o do modo de aquecimento", + "kalibration_kombibetrieb": "Calibra\u00e7\u00e3o do modo combinado", + "kalibration_warmwasserbetrieb": "Calibra\u00e7\u00e3o DHW", + "kaskadenbetrieb": "Opera\u00e7\u00e3o em cascata", + "kombibetrieb": "Modo combinado", + "kombigerat": "Boiler combinado", + "kombigerat_mit_solareinbindung": "Boiler combinado com integra\u00e7\u00e3o solar", + "mindest_kombizeit": "Tempo m\u00ednimo combinado", + "nachlauf_heizkreispumpe": "Funcionamento da bomba do circuito de aquecimento", + "nachspulen": "P\u00f3s-lavagem", + "nur_heizgerat": "Apenas Boiler", + "parallelbetrieb": "Modo paralelo", + "partymodus": "Modo festa", + "perm_cooling": "PermCooling", + "permanent": "Permanente", + "permanentbetrieb": "Modo permanente", + "reduzierter_betrieb": "Modo limitado", + "rt_abschaltung": "Desligamento do RT", + "rt_frostschutz": "RT prote\u00e7\u00e3o contra congelamento", + "ruhekontakt": "Descansar contato", + "schornsteinfeger": "Teste de emiss\u00f5es", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "softstart": "In\u00edcio suave", + "solarbetrieb": "Modo solar", + "sparbetrieb": "Modo econ\u00f4mico", + "sparen": "Economia", + "spreizung_hoch": "dT muito largo", + "spreizung_kf": "Espalhe KF", "stabilisierung": "Estabiliza\u00e7\u00e3o", "standby": "Em espera", "start": "Iniciar", "storung": "Falha", + "taktsperre": "Anti-ciclo", + "telefonfernschalter": "Interruptor remoto do telefone", "test": "Teste", + "tpw": "TPW", "urlaubsmodus": "Modo de f\u00e9rias", - "ventilprufung": "Teste de v\u00e1lvula" + "ventilprufung": "Teste de v\u00e1lvula", + "vorspulen": "Enx\u00e1gue de entrada", + "warmwasser": "DHW", + "warmwasser_schnellstart": "in\u00edcio r\u00e1pido DHW", + "warmwasserbetrieb": "Modo DHW", + "warmwassernachlauf": "DHW funcionando", + "warmwasservorrang": "Prioridade de DHW", + "zunden": "Igni\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sk.json b/homeassistant/components/wolflink/translations/sk.json new file mode 100644 index 0000000000000..5ada995aa6ea9 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index c6ac7aceae16b..44fb0f871de21 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,14 +1,18 @@ """Sensor to indicate whether the current day is a workday.""" from __future__ import annotations -from datetime import timedelta +from datetime import date, timedelta import logging from typing import Any import holidays +from holidays import HolidayBase import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import CONF_NAME, WEEKDAYS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -54,7 +58,7 @@ def valid_country(value: Any) -> str: return value -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_COUNTRY): valid_country, vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( @@ -83,29 +87,26 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Workday sensor.""" - add_holidays = config[CONF_ADD_HOLIDAYS] - remove_holidays = config[CONF_REMOVE_HOLIDAYS] - country = config[CONF_COUNTRY] - days_offset = config[CONF_OFFSET] - excludes = config[CONF_EXCLUDES] - province = config.get(CONF_PROVINCE) - sensor_name = config[CONF_NAME] - workdays = config[CONF_WORKDAYS] - - year = (get_date(dt.now()) + timedelta(days=days_offset)).year - obj_holidays = getattr(holidays, country)(years=year) + add_holidays: list[str] = config[CONF_ADD_HOLIDAYS] + remove_holidays: list[str] = config[CONF_REMOVE_HOLIDAYS] + country: str = config[CONF_COUNTRY] + days_offset: int = config[CONF_OFFSET] + excludes: list[str] = config[CONF_EXCLUDES] + province: str | None = config.get(CONF_PROVINCE) + sensor_name: str = config[CONF_NAME] + workdays: list[str] = config[CONF_WORKDAYS] + + year: int = (get_date(dt.now()) + timedelta(days=days_offset)).year + obj_holidays: HolidayBase = getattr(holidays, country)(years=year) if province: - # 'state' and 'prov' are not interchangeable, so need to make - # sure we use the right one - if hasattr(obj_holidays, "PROVINCES") and province in obj_holidays.PROVINCES: - obj_holidays = getattr(holidays, country)(prov=province, years=year) - elif hasattr(obj_holidays, "STATES") and province in obj_holidays.STATES: - obj_holidays = getattr(holidays, country)(state=province, years=year) + if ( + hasattr(obj_holidays, "subdivisions") + and province in obj_holidays.subdivisions + ): + obj_holidays = getattr(holidays, country)(subdiv=province, years=year) else: - _LOGGER.error( - "There is no province/state %s in country %s", province, country - ) + _LOGGER.error("There is no subdivision %s in country %s", province, country) return # Add custom holidays @@ -116,27 +117,29 @@ def setup_platform( # Remove holidays try: - for date in remove_holidays: + for remove_holiday in remove_holidays: try: # is this formatted as a date? - if dt.parse_date(date): + if dt.parse_date(remove_holiday): # remove holiday by date - removed = obj_holidays.pop(date) - _LOGGER.debug("Removed %s", date) + removed = obj_holidays.pop(remove_holiday) + _LOGGER.debug("Removed %s", remove_holiday) else: # remove holiday by name - _LOGGER.debug("Treating '%s' as named holiday", date) - removed = obj_holidays.pop_named(date) + _LOGGER.debug("Treating '%s' as named holiday", remove_holiday) + removed = obj_holidays.pop_named(remove_holiday) for holiday in removed: - _LOGGER.debug("Removed %s by name '%s'", holiday, date) + _LOGGER.debug( + "Removed %s by name '%s'", holiday, remove_holiday + ) except KeyError as unmatched: _LOGGER.warning("No holiday found matching %s", unmatched) except TypeError: _LOGGER.debug("No holidays to remove or invalid holidays") _LOGGER.debug("Found the following holidays for your configuration:") - for date, name in sorted(obj_holidays.items()): - _LOGGER.debug("%s %s", date, name) + for remove_holiday, name in sorted(obj_holidays.items()): + _LOGGER.debug("%s %s", remove_holiday, name) add_entities( [IsWorkdaySensor(obj_holidays, workdays, excludes, days_offset, sensor_name)], @@ -144,7 +147,7 @@ def setup_platform( ) -def day_to_string(day): +def day_to_string(day: int) -> str | None: """Convert day index 0 - 7 to string.""" try: return ALLOWED_DAYS[day] @@ -152,34 +155,35 @@ def day_to_string(day): return None -def get_date(date): +def get_date(input_date: date) -> date: """Return date. Needed for testing.""" - return date + return input_date class IsWorkdaySensor(BinarySensorEntity): """Implementation of a Workday sensor.""" - def __init__(self, obj_holidays, workdays, excludes, days_offset, name): + def __init__( + self, + obj_holidays: HolidayBase, + workdays: list[str], + excludes: list[str], + days_offset: int, + name: str, + ) -> None: """Initialize the Workday sensor.""" - self._name = name + self._attr_name = name self._obj_holidays = obj_holidays self._workdays = workdays self._excludes = excludes self._days_offset = days_offset - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the device.""" - return self._state + self._attr_extra_state_attributes = { + CONF_WORKDAYS: workdays, + CONF_EXCLUDES: excludes, + CONF_OFFSET: days_offset, + } - def is_include(self, day, now): + def is_include(self, day: str, now: date) -> bool: """Check if given day is in the includes list.""" if day in self._workdays: return True @@ -188,7 +192,7 @@ def is_include(self, day, now): return False - def is_exclude(self, day, now): + def is_exclude(self, day: str, now: date) -> bool: """Check if given day is in the excludes list.""" if day in self._excludes: return True @@ -197,28 +201,21 @@ def is_exclude(self, day, now): return False - @property - def extra_state_attributes(self): - """Return the attributes of the entity.""" - # return self._attributes - return { - CONF_WORKDAYS: self._workdays, - CONF_EXCLUDES: self._excludes, - CONF_OFFSET: self._days_offset, - } - - async def async_update(self): + async def async_update(self) -> None: """Get date and look whether it is a holiday.""" # Default is no workday - self._state = False + self._attr_is_on = False # Get ISO day of the week (1 = Monday, 7 = Sunday) - date = get_date(dt.now()) + timedelta(days=self._days_offset) - day = date.isoweekday() - 1 + adjusted_date = get_date(dt.now()) + timedelta(days=self._days_offset) + day = adjusted_date.isoweekday() - 1 day_of_week = day_to_string(day) - if self.is_include(day_of_week, date): - self._state = True + if day_of_week is None: + return + + if self.is_include(day_of_week, adjusted_date): + self._attr_is_on = True - if self.is_exclude(day_of_week, date): - self._state = False + if self.is_exclude(day_of_week, adjusted_date): + self._attr_is_on = False diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 6140abf4f2aef..ca95065e1a98e 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,8 +2,9 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.12"], + "requirements": ["holidays==0.13"], "codeowners": ["@fabaff"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["convertdate", "hijri_converter", "holidays", "korean_lunar_calendar"] } diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 17d861d643221..6a7aba16b9531 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -1,4 +1,5 @@ """Support for XBee Zigbee devices.""" +# pylint: disable=import-error from binascii import hexlify, unhexlify import logging diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json index fbf9cc925baf4..150036129d2b5 100644 --- a/homeassistant/components/xbee/manifest.json +++ b/homeassistant/components/xbee/manifest.json @@ -1,8 +1,10 @@ { + "disabled": "Integration library not compatible with Python 3.10", "domain": "xbee", "name": "XBee", "documentation": "https://www.home-assistant.io/integrations/xbee", "requirements": ["xbee-helper==0.0.7"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["xbee_helper"] } diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 1d1a4b99705dd..9cea60ade8cae 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -1,4 +1,5 @@ """Support for XBee Zigbee sensors.""" +# pylint: disable=import-error from __future__ import annotations from binascii import hexlify diff --git a/homeassistant/components/xbox/translations/el.json b/homeassistant/components/xbox/translations/el.json new file mode 100644 index 0000000000000..93e620b6b5e0b --- /dev/null +++ b/homeassistant/components/xbox/translations/el.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \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.", + "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.", + "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." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json new file mode 100644 index 0000000000000..7f788c1ebb83e --- /dev/null +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/sk.json b/homeassistant/components/xbox/translations/sk.json new file mode 100644 index 0000000000000..c19b1a0b70c70 --- /dev/null +++ b/homeassistant/components/xbox/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/uk.json b/homeassistant/components/xbox/translations/uk.json index a1b3f8340fc88..1828c0737a6ff 100644 --- a/homeassistant/components/xbox/translations/uk.json +++ b/homeassistant/components/xbox/translations/uk.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "create_entry": { "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." diff --git a/homeassistant/components/xbox/translations/zh-Hant.json b/homeassistant/components/xbox/translations/zh-Hant.json index 07fc710408f87..9d348536ec3a9 100644 --- a/homeassistant/components/xbox/translations/zh-Hant.json +++ b/homeassistant/components/xbox/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 94ebef9f24177..f2dacccb7c303 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xbox_live", "requirements": ["xboxapi==2.0.1"], "codeowners": ["@MartinHjelmare"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["xboxapi"] } diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index e235d35237f0f..12958a93825de 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xeoma", "requirements": ["pyxeoma==1.4.1"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyxeoma"] } diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index ae4059728fe6b..0a21bb37d44f0 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -333,8 +333,7 @@ def extra_state_attributes(self): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state is None: + if (state := await self.async_get_last_state()) is None: return self._state = state.state == "on" diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 13444c6ad69b1..bcc3eef933e73 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -7,5 +7,6 @@ "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "zeroconf": ["_miio._udp.local."], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["xiaomi_gateway"] } diff --git a/homeassistant/components/xiaomi_aqara/translations/el.json b/homeassistant/components/xiaomi_aqara/translations/el.json index d35fc5a2b082d..83923b660c2cd 100644 --- a/homeassistant/components/xiaomi_aqara/translations/el.json +++ b/homeassistant/components/xiaomi_aqara/translations/el.json @@ -1,6 +1,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", + "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", "not_xiaomi_aqara": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara, \u03b7 \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 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03b3\u03bd\u03c9\u03c3\u03c4\u03ad\u03c2 \u03c0\u03cd\u03bb\u03b5\u03c2" }, "error": { @@ -13,6 +15,9 @@ "flow_title": "{name}", "step": { "select": { + "data": { + "select_ip": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03c0\u03cd\u03bb\u03b5\u03c2", "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5" }, @@ -26,6 +31,7 @@ }, "user": { "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", "interface": "\u0397 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7", "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Mac (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, diff --git a/homeassistant/components/xiaomi_aqara/translations/pt-BR.json b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json new file mode 100644 index 0000000000000..5e188e545653d --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "not_xiaomi_aqara": "N\u00e3o \u00e9 um Xiaomi Aqara Gateway, o dispositivo descoberto n\u00e3o corresponde aos gateways conhecidos" + }, + "error": { + "discovery_error": "Falha ao descobrir um Xiaomi Aqara Gateway, tente usar o IP do dispositivo que executa o HomeAssistant como interface", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido, veja https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Interface de rede inv\u00e1lida", + "invalid_key": "Chave de API inv\u00e1lida", + "invalid_mac": "Endere\u00e7o Mac inv\u00e1lido" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "Endere\u00e7o IP" + }, + "description": "Execute a configura\u00e7\u00e3o novamente se quiser conectar gateways adicionais", + "title": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar" + }, + "settings": { + "data": { + "key": "A chave do seu gateway", + "name": "Nome do Gateway" + }, + "description": "A chave (senha) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis", + "title": "Xiaomi Aqara Gateway, configura\u00e7\u00f5es opcionais" + }, + "user": { + "data": { + "host": "Endere\u00e7o IP", + "interface": "A interface de rede a ser usada", + "mac": "Endere\u00e7o Mac (opcional)" + }, + "description": "Conecte-se ao seu Xiaomi Aqara Gateway, se os endere\u00e7os IP e MAC ficarem vazios, a descoberta autom\u00e1tica \u00e9 usada", + "title": "Gateway Xiaomi Aqara" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/sk.json b/homeassistant/components/xiaomi_aqara/translations/sk.json new file mode 100644 index 0000000000000..299acb612fbab --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/sk.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 63fc31722535b..2849b249762c1 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -8,6 +8,8 @@ import async_timeout from miio import ( AirFresh, + AirFreshA1, + AirFreshT2017, AirHumidifier, AirHumidifierMiot, AirHumidifierMjjsq, @@ -48,6 +50,8 @@ DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_AIRPURIFIER_3C, MODEL_FAN_1C, MODEL_FAN_P5, @@ -310,7 +314,7 @@ async def async_create_miio_device_and_coordinator( device = AirHumidifier(host, token, model=model) migrate = True # Airpurifiers and Airfresh - elif model in MODEL_AIRPURIFIER_3C: + elif model == MODEL_AIRPURIFIER_3C: device = AirPurifierMB4(host, token) elif model in MODELS_PURIFIER_MIOT: device = AirPurifierMiot(host, token) @@ -318,6 +322,10 @@ async def async_create_miio_device_and_coordinator( device = AirPurifier(host, token) elif model.startswith("zhimi.airfresh."): device = AirFresh(host, token) + elif model == MODEL_AIRFRESH_A1: + device = AirFreshA1(host, token) + elif model == MODEL_AIRFRESH_T2017: + device = AirFreshT2017(host, token) elif ( model in MODELS_VACUUM or model.startswith(ROBOROCK_GENERIC) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index b361e8ba1b306..cc607b3f41901 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -75,7 +75,9 @@ class SetupException(Exception): MODEL_AIRHUMIDIFIER_JSQ1 = "deerma.humidifier.jsq1" MODEL_AIRHUMIDIFIER_MJJSQ = "deerma.humidifier.mjjsq" +MODEL_AIRFRESH_A1 = "dmaker.airfresh.a1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" +MODEL_AIRFRESH_T2017 = "dmaker.airfresh.t2017" MODEL_FAN_1C = "dmaker.fan.1c" MODEL_FAN_P10 = "dmaker.fan.p10" @@ -129,7 +131,9 @@ class SetupException(Exception): MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H, + MODEL_AIRFRESH_A1, MODEL_AIRFRESH_VA2, + MODEL_AIRFRESH_T2017, ] MODELS_HUMIDIFIER_MIIO = [ MODEL_AIRHUMIDIFIER_V1, @@ -383,6 +387,8 @@ class SetupException(Exception): | FEATURE_SET_CLEAN ) +FEATURE_FLAGS_AIRFRESH_A1 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK + FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK @@ -392,6 +398,8 @@ class SetupException(Exception): | FEATURE_SET_EXTRA_FEATURES ) +FEATURE_FLAGS_AIRFRESH_T2017 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK + FEATURE_FLAGS_FAN_P5 = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index a12a8a6063bb5..e4b79ed77a829 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -5,14 +5,14 @@ import math from miio.airfresh import OperationMode as AirfreshOperationMode +from miio.airfresh_t2017 import OperationMode as AirfreshOperationModeT2017 from miio.airpurifier import OperationMode as AirpurifierOperationMode from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode -from miio.fan import ( +from miio.fan_common import ( MoveDirection as FanMoveDirection, OperationMode as FanOperationMode, ) -from miio.fan_miot import ( - OperationMode as FanMiotOperationMode, +from miio.integrations.fan.zhimi.zhimi_miot import ( OperationModeFanZA5 as FanZA5OperationMode, ) import voluptuous as vol @@ -39,6 +39,8 @@ CONF_FLOW_TYPE, DOMAIN, FEATURE_FLAGS_AIRFRESH, + FEATURE_FLAGS_AIRFRESH_A1, + FEATURE_FLAGS_AIRFRESH_T2017, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_MIIO, @@ -56,6 +58,8 @@ FEATURE_SET_EXTRA_FEATURES, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, @@ -97,6 +101,9 @@ ATTR_USE_TIME = "use_time" ATTR_BUTTON_PRESSED = "button_pressed" +# Air Fresh A1 +ATTR_FAVORITE_SPEED = "favorite_speed" + # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { ATTR_EXTRA_FEATURES: "extra_features", @@ -153,6 +160,7 @@ "Strong", ] PRESET_MODES_AIRFRESH = ["Auto", "Interval"] +PRESET_MODES_AIRFRESH_A1 = ["Auto", "Sleep", "Favorite"] AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) @@ -213,6 +221,10 @@ async def async_setup_entry( entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) elif model.startswith("zhimi.airfresh."): entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + elif model == MODEL_AIRFRESH_A1: + entity = XiaomiAirFreshA1(name, device, config_entry, unique_id, coordinator) + elif model == MODEL_AIRFRESH_T2017: + entity = XiaomiAirFreshT2017(name, device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_P5: entity = XiaomiFanP5(name, device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIIO: @@ -709,6 +721,89 @@ async def async_reset_filter(self): ) +class XiaomiAirFreshA1(XiaomiGenericAirPurifier): + """Representation of a Xiaomi Air Fresh A1.""" + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize the miio device.""" + super().__init__(name, device, entry, unique_id, coordinator) + self._favorite_speed = None + self._device_features = FEATURE_FLAGS_AIRFRESH_A1 + self._preset_modes = PRESET_MODES_AIRFRESH_A1 + self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE + + self._state = self.coordinator.data.is_on + self._mode = self.coordinator.data.mode.value + self._speed_range = (60, 150) + + @property + def operation_mode_class(self): + """Hold operation mode class.""" + return AirfreshOperationModeT2017 + + @property + def percentage(self): + """Return the current percentage based speed.""" + if self._favorite_speed is None: + return None + if self._state: + return ranged_value_to_percentage(self._speed_range, self._favorite_speed) + + return None + + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. This method is a coroutine.""" + if percentage == 0: + await self.async_turn_off() + return + + await self.async_set_preset_mode("Favorite") + + favorite_speed = math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ) + if not favorite_speed: + return + if await self._try_command( + "Setting fan level of the miio device failed.", + self._device.set_favorite_speed, + favorite_speed, + ): + self._favorite_speed = favorite_speed + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. This method is a coroutine.""" + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + if await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, + self.operation_mode_class[preset_mode], + ): + self._mode = self.operation_mode_class[preset_mode].value + self.async_write_ha_state() + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._state = self.coordinator.data.is_on + self._mode = self.coordinator.data.mode.value + self._favorite_speed = getattr(self.coordinator.data, ATTR_FAVORITE_SPEED, None) + self.async_write_ha_state() + + +class XiaomiAirFreshT2017(XiaomiAirFreshA1): + """Representation of a Xiaomi Air Fresh T2017.""" + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize the miio device.""" + super().__init__(name, device, entry, unique_id, coordinator) + self._device_features = FEATURE_FLAGS_AIRFRESH_T2017 + self._speed_range = (60, 300) + + class XiaomiGenericFan(XiaomiGenericDevice): """Representation of a generic Xiaomi Fan.""" @@ -939,7 +1034,7 @@ class XiaomiFanMiot(XiaomiGenericFan): @property def operation_mode_class(self): """Hold operation mode class.""" - return FanMiotOperationMode + return FanOperationMode @property def preset_mode(self): diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index da2b94f5382a0..0091d58e1e2f8 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,8 +3,9 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.9.2"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["micloud", "miio"] } diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 8939200d1073c..4ffc773d1c11e 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -18,6 +18,8 @@ CONF_MODEL, DOMAIN, FEATURE_FLAGS_AIRFRESH, + FEATURE_FLAGS_AIRFRESH_A1, + FEATURE_FLAGS_AIRFRESH_T2017, FEATURE_FLAGS_AIRHUMIDIFIER_CA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRPURIFIER_2S, @@ -45,6 +47,8 @@ FEATURE_SET_VOLUME, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, @@ -199,7 +203,9 @@ class OscillationAngleValues: } MODEL_TO_FEATURES_MAP = { + MODEL_AIRFRESH_A1: FEATURE_FLAGS_AIRFRESH_A1, MODEL_AIRFRESH_VA2: FEATURE_FLAGS_AIRFRESH, + MODEL_AIRFRESH_T2017: FEATURE_FLAGS_AIRFRESH_T2017, MODEL_AIRHUMIDIFIER_CA1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRHUMIDIFIER_CA4: FEATURE_FLAGS_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index a0ff320e228f2..2b5f6f3d5fd20 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -8,7 +8,7 @@ from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness from miio.airpurifier import LedBrightness as AirpurifierLedBrightness from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness -from miio.fan import LedBrightness as FanLedBrightness +from miio.fan_common import LedBrightness as FanLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index d6d1c8500ed41..cbab107994bb0 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -34,6 +34,7 @@ POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, + TIME_DAYS, TIME_HOURS, TIME_SECONDS, VOLUME_CUBIC_METERS, @@ -52,6 +53,8 @@ DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, @@ -91,9 +94,15 @@ ATTR_BATTERY = "battery" ATTR_CARBON_DIOXIDE = "co2" ATTR_CHARGING = "charging" +ATTR_CONTROL_SPEED = "control_speed" ATTR_DISPLAY_CLOCK = "display_clock" +ATTR_FAVORITE_SPEED = "favorite_speed" ATTR_FILTER_LIFE_REMAINING = "filter_life_remaining" ATTR_FILTER_HOURS_USED = "filter_hours_used" +ATTR_DUST_FILTER_LIFE_REMAINING = "dust_filter_life_remaining" +ATTR_DUST_FILTER_LIFE_REMAINING_DAYS = "dust_filter_life_remaining_days" +ATTR_UPPER_FILTER_LIFE_REMAINING = "upper_filter_life_remaining" +ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS = "upper_filter_life_remaining_days" ATTR_FILTER_USE = "filter_use" ATTR_HUMIDITY = "humidity" ATTR_ILLUMINANCE = "illuminance" @@ -105,6 +114,7 @@ ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" ATTR_PM25 = "pm25" +ATTR_PM25_2 = "pm25_2" ATTR_POWER = "power" ATTR_PRESSURE = "pressure" ATTR_PURIFY_VOLUME = "purify_volume" @@ -181,6 +191,22 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + ATTR_CONTROL_SPEED: XiaomiMiioSensorDescription( + key=ATTR_CONTROL_SPEED, + name="Control Speed", + native_unit_of_measurement="rpm", + icon="mdi:fast-forward", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ATTR_FAVORITE_SPEED: XiaomiMiioSensorDescription( + key=ATTR_FAVORITE_SPEED, + name="Favorite Speed", + native_unit_of_measurement="rpm", + icon="mdi:fast-forward", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, name="Motor Speed", @@ -233,6 +259,13 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), + ATTR_PM25_2: XiaomiMiioSensorDescription( + key=ATTR_PM25, + name="PM2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, name="Filter Life Remaining", @@ -250,6 +283,40 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + ATTR_DUST_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( + key=ATTR_DUST_FILTER_LIFE_REMAINING, + name="Dust filter life remaining", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:air-filter", + state_class=SensorStateClass.MEASUREMENT, + attributes=("filter_type",), + entity_category=EntityCategory.DIAGNOSTIC, + ), + ATTR_DUST_FILTER_LIFE_REMAINING_DAYS: XiaomiMiioSensorDescription( + key=ATTR_DUST_FILTER_LIFE_REMAINING_DAYS, + name="Dust filter life remaining days", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:clock-outline", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ATTR_UPPER_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( + key=ATTR_UPPER_FILTER_LIFE_REMAINING, + name="Upper filter life remaining", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:air-filter", + state_class=SensorStateClass.MEASUREMENT, + attributes=("filter_type",), + entity_category=EntityCategory.DIAGNOSTIC, + ), + ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS: XiaomiMiioSensorDescription( + key=ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS, + name="Upper filter life remaining days", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:clock-outline", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, name="Carbon Dioxide", @@ -375,6 +442,26 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): ATTR_TEMPERATURE, ATTR_USE_TIME, ) +AIRFRESH_SENSORS_A1 = ( + ATTR_CARBON_DIOXIDE, + ATTR_DUST_FILTER_LIFE_REMAINING, + ATTR_DUST_FILTER_LIFE_REMAINING_DAYS, + ATTR_PM25_2, + ATTR_TEMPERATURE, + ATTR_CONTROL_SPEED, + ATTR_FAVORITE_SPEED, +) +AIRFRESH_SENSORS_T2017 = ( + ATTR_CARBON_DIOXIDE, + ATTR_DUST_FILTER_LIFE_REMAINING, + ATTR_DUST_FILTER_LIFE_REMAINING_DAYS, + ATTR_UPPER_FILTER_LIFE_REMAINING, + ATTR_UPPER_FILTER_LIFE_REMAINING_DAYS, + ATTR_PM25_2, + ATTR_TEMPERATURE, + ATTR_CONTROL_SPEED, + ATTR_FAVORITE_SPEED, +) FAN_V2_V3_SENSORS = ( ATTR_BATTERY, ATTR_HUMIDITY, @@ -384,7 +471,9 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): FAN_ZA5_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) MODEL_TO_SENSORS_MAP = { + MODEL_AIRFRESH_A1: AIRFRESH_SENSORS_A1, MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, + MODEL_AIRFRESH_T2017: AIRFRESH_SENSORS_T2017, MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRPURIFIER_3C: PURIFIER_3C_SENSORS, @@ -508,7 +597,6 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Main Brush Left", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( @@ -517,7 +605,6 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Side Brush Left", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( @@ -526,7 +613,6 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Filter Left", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( @@ -535,7 +621,6 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Sensor Dirty Left", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -808,7 +893,6 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): def __init__(self, gateway_device, gateway_name, gateway_device_id, description): """Initialize the entity.""" - self._attr_name = f"{gateway_name} {description.name}" self._attr_unique_id = f"{gateway_device_id}-{description.key}" self._attr_device_info = {"identifiers": {(DOMAIN, gateway_device_id)}} diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index bd6482e891b7c..ce05a891c0ac7 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -35,6 +35,8 @@ CONF_MODEL, DOMAIN, FEATURE_FLAGS_AIRFRESH, + FEATURE_FLAGS_AIRFRESH_A1, + FEATURE_FLAGS_AIRFRESH_T2017, FEATURE_FLAGS_AIRHUMIDIFIER, FEATURE_FLAGS_AIRHUMIDIFIER_CA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, @@ -63,6 +65,8 @@ FEATURE_SET_LED, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, @@ -167,7 +171,9 @@ } MODEL_TO_FEATURES_MAP = { + MODEL_AIRFRESH_A1: FEATURE_FLAGS_AIRFRESH_A1, MODEL_AIRFRESH_VA2: FEATURE_FLAGS_AIRFRESH, + MODEL_AIRFRESH_T2017: FEATURE_FLAGS_AIRFRESH_T2017, MODEL_AIRHUMIDIFIER_CA1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRHUMIDIFIER_CA4: FEATURE_FLAGS_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json index ec275b9333036..69b6cd221ee0f 100644 --- a/homeassistant/components/xiaomi_miio/translations/cs.json +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -2,19 +2,41 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "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" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed." + "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed.", + "unknown_device": "Model za\u0159\u00edzen\u00ed nen\u00ed zn\u00e1m, nastaven\u00ed za\u0159\u00edzen\u00ed nen\u00ed mo\u017en\u00e9 dokon\u010dit.", + "wrong_token": "Chyba kontroln\u00edho sou\u010dtu, \u0161patn\u00fd token" }, "flow_title": "Xiaomi Miio: {name}", "step": { + "cloud": { + "data": { + "manual": "Nastavit ru\u010dn\u011b (nedoporu\u010deno)" + }, + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + }, + "connect": { + "data": { + "model": "Model za\u0159\u00edzen\u00ed" + }, + "description": "Ru\u010dn\u011b vyberte model za\u0159\u00edzen\u00ed ze seznamu podporovan\u00fdch za\u0159\u00edzen\u00ed.", + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + }, "device": { "data": { "host": "IP adresa", + "model": "Model za\u0159\u00edzen\u00ed (voliteln\u00e9)", + "name": "Jm\u00e9no za\u0159\u00edzen\u00ed", "token": "API token" - } + }, + "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" }, "gateway": { "data": { @@ -25,6 +47,24 @@ "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", "title": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" }, + "manual": { + "data": { + "host": "IP adresa", + "token": "API token" + }, + "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, + "select": { + "data": { + "select_device": "Za\u0159\u00edzen\u00ed Miio" + }, + "description": "Vyberte Xiaomi Miio za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit.", + "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + }, "user": { "data": { "gateway": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" @@ -33,5 +73,16 @@ "title": "Xiaomi Miio" } } + }, + "options": { + "step": { + "init": { + "data": { + "cloud_subdevices": "Pou\u017e\u00edt cloud pro z\u00edsk\u00e1n\u00ed p\u0159ipojen\u00fdch podru\u017en\u00fdch za\u0159\u00edzen\u00ed" + }, + "description": "Zadejte voliteln\u00e9 nastaven\u00ed", + "title": "Xiaomi Miio" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/el.json b/homeassistant/components/xiaomi_miio/translations/el.json index c5c8d79c281d1..abd099a404b05 100644 --- a/homeassistant/components/xiaomi_miio/translations/el.json +++ b/homeassistant/components/xiaomi_miio/translations/el.json @@ -1,17 +1,98 @@ { "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", + "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" + }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "cloud_credentials_incomplete": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 Cloud \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae, \u03c3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03c7\u03ce\u03c1\u03b1", + "cloud_login_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Xiaomi Miio Cloud, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1.", + "cloud_no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Xiaomi Miio cloud.", "no_device_selected": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03b5\u03af \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", - "unknown_device": "\u03a4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03bd\u03c9\u03c3\u03c4\u03cc, \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03bc\u03b5 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd." + "unknown_device": "\u03a4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03bd\u03c9\u03c3\u03c4\u03cc, \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03bc\u03b5 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd.", + "wrong_token": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b8\u03c1\u03bf\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5, \u03bb\u03ac\u03b8\u03bf\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" }, + "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "\u03a7\u03ce\u03c1\u03b1 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae cloud", + "cloud_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Cloud", + "cloud_username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Cloud", + "manual": "\u039c\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 (\u03b4\u03b5\u03bd \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud Xiaomi Miio, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.openhab.org/addons/bindings/miio/#country-servers \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae cloud \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "connect": { + "data": { + "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03b1.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, "device": { "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" }, "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "gateway": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 32 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API , \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "manual": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 xiaomi Miio \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac \u03ae \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 cloud \u03c0\u03bf\u03c5 \u03bb\u03b5\u03af\u03c0\u03bf\u03c5\u03bd.", + "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" + }, + "select": { + "data": { + "select_device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Miio" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "user": { + "data": { + "gateway": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 Xiaomi" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5.", + "title": "Xiaomi Miio" + } + } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 cloud \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae, \u03c3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03c7\u03ce\u03c1\u03b1" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf cloud \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ce\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 35bdecea0822d..b643f7df4dec0 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -52,7 +52,7 @@ "token": "Token API" }, "description": "\u00c8 necessario il Token API di 32 caratteri, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Nota che questo Token API \u00e8 diverso dalla chiave usata dall'integrazione di Xiaomi Aqara.", - "title": "Connessione a un Xiaomi Gateway " + "title": "Connettiti a un Xiaomi Gateway " }, "manual": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json index beeb45b988071..9e966a541d574 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json @@ -1,13 +1,98 @@ { "config": { "abort": { - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para este dispositivo Xiaomi Miio 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", + "incomplete_info": "Informa\u00e7\u00f5es incompletas para configurar o dispositivo, nenhum host ou token fornecido.", + "not_xiaomi_miio": "O dispositivo (ainda) n\u00e3o \u00e9 suportado pelo Xiaomi Miio.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, + "error": { + "cannot_connect": "Falha ao conectar", + "cloud_credentials_incomplete": "Credenciais da nuvem incompletas, preencha o nome de usu\u00e1rio, a senha e o pa\u00eds", + "cloud_login_error": "N\u00e3o foi poss\u00edvel fazer login no Xiaomi Miio Cloud, verifique as credenciais.", + "cloud_no_devices": "Nenhum dispositivo encontrado nesta conta de nuvem Xiaomi Miio.", + "no_device_selected": "Nenhum dispositivo selecionado, selecione um dispositivo.", + "unknown_device": "O modelo do dispositivo n\u00e3o \u00e9 conhecido, n\u00e3o \u00e9 poss\u00edvel configurar o dispositivo usando o fluxo de configura\u00e7\u00e3o.", + "wrong_token": "Erro de checksum, token errado" + }, + "flow_title": "{name}", "step": { + "cloud": { + "data": { + "cloud_country": "Pa\u00eds do servidor em Cloud", + "cloud_password": "Senha da Cloud", + "cloud_username": "Usu\u00e1rio da Cloud", + "manual": "Configurar manualmente (n\u00e3o recomendado)" + }, + "description": "Fa\u00e7a login na Cloud Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para o servidor em cloud usar.", + "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + }, + "connect": { + "data": { + "model": "Modelo do dispositivo" + }, + "description": "Selecione manualmente o modelo do dispositivo entre os modelos suportados.", + "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + }, + "device": { + "data": { + "host": "Endere\u00e7o IP", + "model": "Modelo do dispositivo (opcional)", + "name": "Nome do dispositivo", + "token": "Token da API" + }, + "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", + "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + }, "gateway": { "data": { - "host": "Endere\u00e7o IP" - } + "host": "Endere\u00e7o IP", + "name": "Nome do Gateway", + "token": "Token da API" + }, + "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", + "title": "Conecte-se a um Xiaomi Gateway" + }, + "manual": { + "data": { + "host": "Endere\u00e7o IP", + "token": "Token da API" + }, + "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", + "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Xiaomi Miio precisa autenticar novamente sua conta para atualizar os tokens ou adicionar credenciais de nuvem ausentes.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "select": { + "data": { + "select_device": "Dispositivo Miio" + }, + "description": "Selecione o dispositivo Xiaomi Miio para configurar.", + "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Conecte-se a um Xiaomi Gateway" + }, + "description": "Selecione a qual dispositivo voc\u00ea deseja se conectar.", + "title": "Xiaomi Miio" + } + } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Credenciais da cloud incompletas, preencha o nome de usu\u00e1rio, a senha e o pa\u00eds" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Use a cloud para obter subdispositivos conectados" + }, + "description": "Especificar configura\u00e7\u00f5es opcionais", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.el.json b/homeassistant/components/xiaomi_miio/translations/select.el.json new file mode 100644 index 0000000000000..24c8a0376764a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.el.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "\u03a6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc", + "dim": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json new file mode 100644 index 0000000000000..c4c1735bff370 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Brilhante", + "dim": "Escurecido", + "off": "Desligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sk.json b/homeassistant/components/xiaomi_miio/translations/sk.json new file mode 100644 index 0000000000000..022e4103fb7b9 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/sk.json @@ -0,0 +1,26 @@ +{ + "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" + }, + "step": { + "device": { + "data": { + "token": "API token" + } + }, + "gateway": { + "data": { + "token": "API token" + } + }, + "manual": { + "data": { + "token": "API token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 85fbbef7928a3..303480c2e7f9e 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", "requirements": ["pymitv==1.4.3"], "codeowners": ["@simse"], - "iot_class": "assumed_state" + "iot_class": "assumed_state", + "loggers": ["pymitv"] } diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 55df2587898e9..840f2cd677d16 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xmpp", "requirements": ["slixmpp==1.7.1"], "codeowners": ["@fabaff", "@flowolf"], - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "loggers": ["pyasn1", "slixmpp"] } diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 4cb5770bed7c0..cbc0e147f5b48 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xs1", "requirements": ["xs1-api-client==3.0.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["xs1_api_client"] } diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index 1567f22be4471..62daa639f5026 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -27,7 +27,6 @@ { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string, } ) @@ -45,7 +44,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - entry: ConfigEntry + entry: ConfigEntry | None @staticmethod @callback @@ -53,20 +52,21 @@ def async_get_options_flow(config_entry: ConfigEntry) -> YaleOptionsFlowHandler: """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict): + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import a configuration from config.yaml.""" - self.context.update( - {"title_placeholders": {CONF_NAME: f"YAML import {DOMAIN}"}} - ) return await self.async_step_user(user_input=config) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle initiation of re-authentication with Yale.""" 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=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Dialog that informs the user that reauth is required.""" errors = {} @@ -87,7 +87,7 @@ async def async_step_reauth_confirm(self, user_input=None): if not errors: existing_entry = await self.async_set_unique_id(username) - if existing_entry: + if existing_entry and self.entry: self.hass.config_entries.async_update_entry( existing_entry, data={ @@ -105,14 +105,16 @@ async def async_step_reauth_confirm(self, user_input=None): errors=errors, ) - async def async_step_user(self, user_input=None): + 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: username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - name = user_input.get(CONF_NAME, DEFAULT_NAME) + name = DEFAULT_NAME area = user_input.get(CONF_AREA_ID, DEFAULT_AREA_ID) try: diff --git a/homeassistant/components/yale_smart_alarm/diagnostics.py b/homeassistant/components/yale_smart_alarm/diagnostics.py new file mode 100644 index 0000000000000..896a3240a221d --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/diagnostics.py @@ -0,0 +1,30 @@ +"""Diagnostics support for Yale Smart Alarm.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import COORDINATOR, DOMAIN +from .coordinator import YaleDataUpdateCoordinator + +TO_REDACT = { + "address", + "name", + "mac", + "device_id", + "sensor_map", + "lock_map", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + COORDINATOR + ] + return async_redact_data(coordinator.data, TO_REDACT) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 6bc3846ea67a6..0b1a5a94da09e 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,8 +2,9 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.3.7"], + "requirements": ["yalesmartalarmclient==0.3.8"], "codeowners": ["@gjohansson-ST"], "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "loggers": ["yalesmartalarmclient"] } diff --git a/homeassistant/components/yale_smart_alarm/translations/el.json b/homeassistant/components/yale_smart_alarm/translations/el.json index 6a8ad33c53bc5..fb53e59f71fe0 100644 --- a/homeassistant/components/yale_smart_alarm/translations/el.json +++ b/homeassistant/components/yale_smart_alarm/translations/el.json @@ -1,9 +1,28 @@ { "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", + "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", + "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_confirm": { + "data": { + "area_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { - "area_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2" + "area_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\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", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" } } } diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json index 50ad7f4b9ca4a..76065006684c8 100644 --- a/homeassistant/components/yale_smart_alarm/translations/fr.json +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -11,7 +11,7 @@ "step": { "reauth_confirm": { "data": { - "area_id": "ID de la zone", + "area_id": "ID de zone", "name": "Nom", "password": "Mot de passe", "username": "Nom d'utilisateur" @@ -28,9 +28,13 @@ } }, "options": { + "error": { + "code_format_mismatch": "Le code ne correspond pas au nombre de chiffres requis" + }, "step": { "init": { "data": { + "code": "Code par d\u00e9faut pour les serrures, utilis\u00e9 si aucun n'est donn\u00e9", "lock_code_digits": "Nombre de chiffres dans le code PIN pour les serrures" } } diff --git a/homeassistant/components/yale_smart_alarm/translations/id.json b/homeassistant/components/yale_smart_alarm/translations/id.json index 86e54e111bde8..da01b435335e3 100644 --- a/homeassistant/components/yale_smart_alarm/translations/id.json +++ b/homeassistant/components/yale_smart_alarm/translations/id.json @@ -26,5 +26,18 @@ } } } + }, + "options": { + "error": { + "code_format_mismatch": "Kode tidak sesuai dengan jumlah digit yang diperlukan" + }, + "step": { + "init": { + "data": { + "code": "Kode bawaan untuk kunci, digunakan jika tidak ada yang diberikan", + "lock_code_digits": "Jumlah digit dalam kode PIN untuk kunci" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/nb.json b/homeassistant/components/yale_smart_alarm/translations/nb.json new file mode 100644 index 0000000000000..c106bc179b317 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/nb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "Brukernavn" + } + }, + "user": { + "data": { + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json index cdde7efe15167..04dbe9245c192 100644 --- a/homeassistant/components/yale_smart_alarm/translations/nl.json +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -34,7 +34,8 @@ "step": { "init": { "data": { - "code": "Standaardcode voor sloten, gebruikt als er geen is opgegeven" + "code": "Standaardcode voor sloten, gebruikt als er geen is opgegeven", + "lock_code_digits": "Aantal cijfers in PIN code voor sloten" } } } diff --git a/homeassistant/components/yale_smart_alarm/translations/pl.json b/homeassistant/components/yale_smart_alarm/translations/pl.json index 31897a0d3c986..de49d07361aa0 100644 --- a/homeassistant/components/yale_smart_alarm/translations/pl.json +++ b/homeassistant/components/yale_smart_alarm/translations/pl.json @@ -5,7 +5,7 @@ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { diff --git a/homeassistant/components/yale_smart_alarm/translations/pt-BR.json b/homeassistant/components/yale_smart_alarm/translations/pt-BR.json index b9580fd103fc9..332c91ed44700 100644 --- a/homeassistant/components/yale_smart_alarm/translations/pt-BR.json +++ b/homeassistant/components/yale_smart_alarm/translations/pt-BR.json @@ -1,7 +1,43 @@ { "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, "error": { - "cannot_connect": "Falha na conex\u00e3o" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID da \u00e1rea", + "name": "Nome", + "password": "Senha", + "username": "Usu\u00e1rio" + } + }, + "user": { + "data": { + "area_id": "ID da \u00e1rea", + "name": "Nome", + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "O c\u00f3digo n\u00e3o corresponde ao n\u00famero necess\u00e1rio de d\u00edgitos" + }, + "step": { + "init": { + "data": { + "code": "C\u00f3digo padr\u00e3o para fechaduras, usado se nenhuma for dada", + "lock_code_digits": "N\u00famero de d\u00edgitos no c\u00f3digo PIN para fechaduras" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/sk.json b/homeassistant/components/yale_smart_alarm/translations/sk.json new file mode 100644 index 0000000000000..00dddc88d1d73 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "reauth_confirm": { + "data": { + "name": "N\u00e1zov" + } + }, + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index 437e9479ae18a..7fc86f707b30d 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/yamaha", "requirements": ["rxv==0.7.0"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["rxv"] } diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 7d07d57fc289b..a50ef69d57e0f 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -18,5 +18,6 @@ "codeowners": [ "@vigonotion", "@micha91" - ] + ], + "loggers": ["aiomusiccast"] } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/el.json b/homeassistant/components/yamaha_musiccast/translations/el.json new file mode 100644 index 0000000000000..b164109231ed8 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/el.json @@ -0,0 +1,23 @@ +{ + "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", + "yxc_control_url_missing": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b4\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf\u03c5 ssdp." + }, + "error": { + "no_musiccast_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae MusicCast." + }, + "flow_title": "MusicCast: {name}", + "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;" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf MusicCast \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json new file mode 100644 index 0000000000000..eee52e2182b48 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "yxc_control_url_missing": "A URL de controle n\u00e3o \u00e9 fornecida na descri\u00e7\u00e3o do ssdp." + }, + "error": { + "no_musiccast_device": "Este dispositivo parece n\u00e3o ser um dispositivo MusicCast." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Configure o MusicCast para integrar com o Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.el.json b/homeassistant/components/yamaha_musiccast/translations/select.el.json new file mode 100644 index 0000000000000..6caacd91b6488 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.el.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "bypass": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7", + "manual": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "\u03a3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03ae\u03c7\u03bf\u03c5", + "audio_sync_off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2 \u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03ae\u03c7\u03bf\u03c5", + "audio_sync_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2 \u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03ae\u03c7\u03bf\u03c5", + "balanced": "\u0399\u03c3\u03bf\u03c1\u03c1\u03bf\u03c0\u03b7\u03bc\u03ad\u03bd\u03bf", + "lip_sync": "\u03a3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c7\u03b5\u03b9\u03bb\u03b9\u03ce\u03bd" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u03a3\u03c5\u03bc\u03c0\u03b9\u03b5\u03c3\u03bc\u03ad\u03bd\u03bf", + "uncompressed": "\u039c\u03b7 \u03c3\u03c5\u03bc\u03c0\u03b9\u03b5\u03c3\u03bc\u03ad\u03bd\u03bf" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1", + "stability": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc\u03c4\u03b7\u03c4\u03b1", + "standard": "\u03a4\u03c5\u03c0\u03b9\u03ba\u03cc" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u03bb\u03b5\u03c0\u03c4\u03ac", + "30 min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", + "60 min": "60 \u03bb\u03b5\u03c0\u03c4\u03ac", + "90 min": "90 \u03bb\u03b5\u03c0\u03c4\u03ac", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x \u03a0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9", + "dolby_pl2x_movie": "Dolby ProLogic 2x \u03a4\u03b1\u03b9\u03bd\u03af\u03b1", + "dolby_pl2x_music": "Dolby ProLogic 2x \u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 \u039a\u03b9\u03bd\u03b7\u03bc\u03b1\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03bf\u03c2", + "dts_neo6_music": "DTS Neo:6 \u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae", + "dts_neural_x": "DTS Neural:X", + "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "bypass": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7", + "manual": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.lv.json b/homeassistant/components/yamaha_musiccast/translations/select.lv.json new file mode 100644 index 0000000000000..17d5aa4c832a1 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.lv.json @@ -0,0 +1,13 @@ +{ + "state": { + "yamaha_musiccast__zone_link_control": { + "speed": "\u0100trums" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 min\u016btes", + "30 min": "30 min\u016btes", + "60 min": "60 min\u016btes", + "90 min": "90 min\u016btes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pt-BR.json b/homeassistant/components/yamaha_musiccast/translations/select.pt-BR.json new file mode 100644 index 0000000000000..dfbd33784b5fe --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.pt-BR.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Auto" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Auto", + "bypass": "Contornar", + "manual": "Manual" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Sincroniza\u00e7\u00e3o de \u00e1udio", + "audio_sync_off": "Sincroniza\u00e7\u00e3o de \u00e1udio desligada", + "audio_sync_on": "Sincroniza\u00e7\u00e3o de \u00e1udio ligada", + "balanced": "Equilibrado", + "lip_sync": "Sincroniza\u00e7\u00e3o labial" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Comprimido", + "uncompressed": "Descomprimido" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Velocidade", + "stability": "Estabilidade", + "standard": "Padr\u00e3o" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minutos", + "30 min": "30 minutos", + "60 min": "60 minutos", + "90 min": "90 minutos", + "off": "Desligado" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Autom\u00e1tico", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Game", + "dolby_pl2x_movie": "Dolby ProLogic 2x Movie", + "dolby_pl2x_music": "Dolby ProLogic 2x Music", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Music", + "dts_neural_x": "DTS Neural:X", + "toggle": "Alternar" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Autom\u00e1tico", + "bypass": "Bypass", + "manual": "Manual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index d1b8e9d4f4615..4d226f233b28a 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -230,7 +230,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 0419824492afa..8dd127502e2a6 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Yeelight integration.""" +import asyncio import logging from urllib.parse import urlparse @@ -86,11 +87,13 @@ async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowRes async def _async_handle_discovery_with_unique_id(self): """Handle any discovery with a unique id.""" - for entry in self._async_current_entries(): - if entry.unique_id != self.unique_id: + for entry in self._async_current_entries(include_ignore=False): + if entry.unique_id != self.unique_id and self.unique_id != entry.data.get( + CONF_ID + ): continue reload = entry.state == ConfigEntryState.SETUP_RETRY - if entry.data[CONF_HOST] != self._discovered_ip: + if entry.data.get(CONF_HOST) != self._discovered_ip: self.hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_HOST: self._discovered_ip} ) @@ -261,7 +264,7 @@ async def _async_try_connect(self, host, raise_on_progress=True): await bulb.async_listen(lambda _: True) await bulb.async_get_properties() await bulb.async_stop_listening() - except yeelight.BulbException as err: + except (asyncio.TimeoutError, yeelight.BulbException) as err: _LOGGER.error("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 84b76d9865848..0c906b3e268cd 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -7,7 +7,8 @@ import voluptuous as vol import yeelight -from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows +from yeelight import Flow, RGBTransition, SleepTransition, flows +from yeelight.aio import AsyncBulb from yeelight.enums import BulbType, LightType, PowerMode, SceneClass from yeelight.main import BulbException @@ -549,7 +550,7 @@ def effect(self): return self._effect if self.device.is_color_flow_enabled else None @property - def _bulb(self) -> Bulb: + def _bulb(self) -> AsyncBulb: return self.device.bulb @property @@ -608,8 +609,10 @@ async def async_set_music_mode(self, music_mode) -> None: async def _async_set_music_mode(self, music_mode) -> None: """Set the music mode on or off wrapped with _async_cmd.""" bulb = self._bulb - method = bulb.stop_music if not music_mode else bulb.start_music - await self.hass.async_add_executor_job(method) + if music_mode: + await bulb.async_start_music() + else: + await bulb.async_stop_music() @_async_cmd async def async_set_brightness(self, brightness, duration) -> None: diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 5320b8023e9c5..7aa468819686b 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,8 +2,8 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.23.4"], - "codeowners": ["@zewelor", "@shenxn", "@starkillerOG"], + "requirements": ["yeelight==0.7.9", "async-upnp-client==0.23.5"], + "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], "quality_scale": "platinum", @@ -17,5 +17,6 @@ "homekit": { "models": ["YL*"] }, - "after_dependencies": ["ssdp"] + "after_dependencies": ["ssdp"], + "loggers": ["async_upnp_client", "yeelight"] } diff --git a/homeassistant/components/yeelight/translations/el.json b/homeassistant/components/yeelight/translations/el.json index cfd4d626b5a67..b125dae0bc606 100644 --- a/homeassistant/components/yeelight/translations/el.json +++ b/homeassistant/components/yeelight/translations/el.json @@ -1,5 +1,9 @@ { "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", + "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" }, @@ -14,6 +18,9 @@ } }, "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, "description": "\u0395\u03ac\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } diff --git a/homeassistant/components/yeelight/translations/pt-BR.json b/homeassistant/components/yeelight/translations/pt-BR.json new file mode 100644 index 0000000000000..2c54af41b250a --- /dev/null +++ b/homeassistant/components/yeelight/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "flow_title": "{model} {id} ({host})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Nome do host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para encontrar dispositivos." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modelo", + "nightlight_switch": "Use o interruptor de luz noturna", + "save_on_change": "Salvar status na altera\u00e7\u00e3o", + "transition": "Tempo de transi\u00e7\u00e3o (ms)", + "use_music_mode": "Ativar o modo de m\u00fasica" + }, + "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sk.json b/homeassistant/components/yeelight/translations/sk.json new file mode 100644 index 0000000000000..793f8eff27872 --- /dev/null +++ b/homeassistant/components/yeelight/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 17156ae3490ac..edae33b75aaa5 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", "requirements": ["yeelightsunflower==0.0.10"], "codeowners": ["@lindsaymarkward"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["yeelightsunflower"] } diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index 140b1cf3132ea..232995427365d 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aioftp==0.12.0"], "dependencies": ["ffmpeg"], "codeowners": ["@bachya"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["aioftp"] } diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index f5713c51680ef..1e952452f46d6 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/youless", "requirements": ["youless-api==0.16"], "codeowners": ["@gjong"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["youless_api"] } diff --git a/homeassistant/components/youless/translations/el.json b/homeassistant/components/youless/translations/el.json new file mode 100644 index 0000000000000..beb582b14eacb --- /dev/null +++ b/homeassistant/components/youless/translations/el.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/pt-BR.json b/homeassistant/components/youless/translations/pt-BR.json new file mode 100644 index 0000000000000..ec60fefab4277 --- /dev/null +++ b/homeassistant/components/youless/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/sk.json b/homeassistant/components/youless/translations/sk.json new file mode 100644 index 0000000000000..af15f92c2f27a --- /dev/null +++ b/homeassistant/components/youless/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zov" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index 39f8ebae4aeae..8101fd6bf796f 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/zabbix", "requirements": ["py-zabbix==1.1.7"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["pyzabbix"] } diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index 45cf866f51f25..98f2ab1de21e9 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": ["zengge==0.2"], "codeowners": [], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["zengge"] } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 1dc70cde610d2..ffe4140a434aa 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -101,6 +101,7 @@ class ZeroconfServiceInfo(BaseServiceInfo): """Prepared info from mDNS entries.""" host: str + addresses: list[str] port: int | None hostname: str type: str @@ -540,13 +541,14 @@ def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None: if isinstance(value, bytes): properties[key] = value.decode("utf-8") - if not (addresses := service.addresses): + if not (addresses := service.addresses or service.parsed_addresses()): return None - if (host := _first_non_link_local_or_v6_address(addresses)) is None: + if (host := _first_non_link_local_address(addresses)) is None: return None return ZeroconfServiceInfo( host=str(host), + addresses=service.parsed_addresses(), port=service.port, hostname=service.server, type=service.type, @@ -555,11 +557,18 @@ def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None: ) -def _first_non_link_local_or_v6_address(addresses: list[bytes]) -> str | None: - """Return the first ipv6 or non-link local ipv4 address.""" +def _first_non_link_local_address( + addresses: list[bytes] | list[str], +) -> str | None: + """Return the first ipv6 or non-link local ipv4 address, preferring IPv4.""" + for address in addresses: + ip_addr = ip_address(address) + if not ip_addr.is_link_local and ip_addr.version == 4: + return str(ip_addr) + # If we didn't find a good IPv4 address, check for IPv6 addresses. for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local or ip_addr.version == 6: + if not ip_addr.is_link_local and ip_addr.version == 6: return str(ip_addr) return None diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6f6a56774d735..dc4c7c001aeab 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,9 +2,10 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.3"], + "requirements": ["zeroconf==0.38.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["zeroconf"] } diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index ab0a0eaf9a77c..47798be3def4e 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -23,5 +23,5 @@ def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: def new_zeroconf_init(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> None: return - zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore - zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore + zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore[assignment] + zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore[assignment] diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index fdf17c14e5a25..e68c51cd7eb47 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -3,6 +3,7 @@ import pyzerproc +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from .const import DOMAIN @@ -10,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) -async def _async_has_devices(hass) -> bool: +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" try: devices = await pyzerproc.discover() diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index dfaf6587d3b18..eb43edc7fec5b 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": ["pyzerproc==0.4.8"], "codeowners": ["@emlove"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["bleak", "pyzerproc"] } diff --git a/homeassistant/components/zerproc/translations/el.json b/homeassistant/components/zerproc/translations/el.json new file mode 100644 index 0000000000000..a13912159002b --- /dev/null +++ b/homeassistant/components/zerproc/translations/el.json @@ -0,0 +1,13 @@ +{ + "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." + }, + "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;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/pt-BR.json b/homeassistant/components/zerproc/translations/pt-BR.json index 4cbb697371ed2..1778d39a7d082 100644 --- a/homeassistant/components/zerproc/translations/pt-BR.json +++ b/homeassistant/components/zerproc/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo encontrado na rede", - "single_instance_allowed": "J\u00e1 configurado. Somente uma \u00fanica configura\u00e7\u00e3o poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/zerproc/translations/uk.json b/homeassistant/components/zerproc/translations/uk.json index 292861e9129db..5c2489c2a18ab 100644 --- a/homeassistant/components/zerproc/translations/uk.json +++ b/homeassistant/components/zerproc/translations/uk.json @@ -2,7 +2,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": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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": { diff --git a/homeassistant/components/zerproc/translations/zh-Hant.json b/homeassistant/components/zerproc/translations/zh-Hant.json index 90c98e491dfea..cfd20d603cba1 100644 --- a/homeassistant/components/zerproc/translations/zh-Hant.json +++ b/homeassistant/components/zerproc/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 65de5fd04cc79..d7e36c52517a1 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -431,14 +431,10 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: self.debug("preset mode '%s' is not supported", preset_mode) return - if ( - self.preset_mode - not in ( - preset_mode, - PRESET_NONE, - ) - and not await self.async_preset_handler(self.preset_mode, enable=False) - ): + if self.preset_mode not in ( + preset_mode, + PRESET_NONE, + ) and not await self.async_preset_handler(self.preset_mode, enable=False): self.debug("Couldn't turn off '%s' preset", self.preset_mode) return @@ -760,3 +756,18 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: ) return False + + +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers="Stelpro", + models={"SORB"}, + stop_on_match_group=CHANNEL_THERMOSTAT, +) +class StelproFanHeater(Thermostat): + """Stelpro Fan Heater implementation.""" + + @property + def hvac_modes(self) -> tuple[str, ...]: + """Return only the heat mode, because the device can't be turned off.""" + return (HVAC_MODE_HEAT,) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index dfc1ffba538a7..e542c77516e37 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,11 +7,11 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.66", + "zha-quirks==0.0.67", "zigpy-deconz==0.14.0", "zigpy==0.43.0", "zigpy-xbee==0.14.0", - "zigpy-zigate==0.7.3", + "zigpy-zigate==0.8.0", "zigpy-znp==0.7.0" ], "usb": [ @@ -72,5 +72,17 @@ } ], "after_dependencies": ["usb", "zeroconf"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": [ + "aiosqlite", + "bellows", + "crccheck", + "pure_pcapy3", + "zhaquirks", + "zigpy", + "zigpy_deconz", + "zigpy_xbee", + "zigpy_zigate", + "zigpy_znp" + ] } diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 2a58de56c32e1..e0fb76cd6cb4b 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -2,8 +2,13 @@ "config": { "abort": { "not_zha_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae zha", + "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.", "usb_probe_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 usb" }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{name}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ;" @@ -28,7 +33,8 @@ "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" + "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" } } }, @@ -40,6 +46,8 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd" }, "zha_options": { + "consider_unavailable_battery": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", + "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" @@ -76,8 +84,13 @@ }, "trigger_type": { "device_dropped": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c0\u03b5\u03c3\u03b5", + "device_flipped": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03bf\u03b4\u03bf\u03b3\u03cd\u03c1\u03b9\u03c3\u03b5 \"{subtype}\"", + "device_knocked": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c4\u03cd\u03c0\u03b7\u03c3\u03b5 \"{subtype}\"", + "device_offline": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "device_rotated": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03ac\u03c6\u03b7\u03ba\u03b5 \"{subtype}\"", "device_shaken": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03ba\u03b9\u03bd\u03ae\u03b8\u03b7\u03ba\u03b5", + "device_slid": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03bb\u03af\u03c3\u03c4\u03c1\u03b7\u03c3\u03b5 \"{subtype}\"", + "device_tilted": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03af\u03c3\u03b7", "remote_button_alt_double_press": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03c0\u03bb\u03ac (\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", "remote_button_alt_long_press": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c5\u03bd\u03b5\u03c7\u03ce\u03c2 (\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", "remote_button_alt_long_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{\u03c3\u03b8\u03b2\u03c4\u03c5\u03c0\u03b5}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1 (\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 884c6429cadc0..504ae7eadbaf4 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -84,8 +84,8 @@ }, "trigger_type": { "device_dropped": "Dispositivo caduto", - "device_flipped": "Dispositivo capovolto \" {subtype} \"", - "device_knocked": "Dispositivo bussato \" {subtype} \"", + "device_flipped": "Dispositivo capovolto \"{subtype}\"", + "device_knocked": "Dispositivo bussato \"{subtype}\"", "device_offline": "Dispositivo offline", "device_rotated": "Dispositivo ruotato \" {subtype} \"", "device_shaken": "Dispositivo in vibrazione", diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index e06bff43993c7..2e5aec0d1cce5 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do ZHA \u00e9 permitida." + "not_zha_device": "Este dispositivo n\u00e3o \u00e9 um dispositivo ZHA", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "usb_probe_failed": "Falha ao sondar o dispositivo usb" }, "error": { - "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao dispositivo ZHA." + "cannot_connect": "Falha ao conectar" }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "Voc\u00ea deseja configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo de r\u00e1dio" @@ -16,25 +22,91 @@ }, "port_config": { "data": { - "baudrate": "velocidade da porta" + "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" }, "user": { + "data": { + "path": "Caminho do dispositivo serial" + }, + "description": "Selecione a porta serial para o r\u00e1dio Zigbee", "title": "ZHA" } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "C\u00f3digo necess\u00e1rio para a\u00e7\u00f5es de armamento", + "alarm_failed_tries": "O n\u00famero de entradas consecutivas de c\u00f3digo com falha para acionar um alarme", + "alarm_master_code": "C\u00f3digo mestre para o(s) painel(es) de controle de alarme", + "title": "Op\u00e7\u00f5es do painel de controle de alarme" + }, + "zha_options": { + "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", + "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", + "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", + "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", + "title": "Op\u00e7\u00f5es globais" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", "warn": "Aviso" }, "trigger_subtype": { - "close": "Fechado" + "both_buttons": "Ambos os bot\u00f5es", + "button_1": "Primeiro bot\u00e3o", + "button_2": "Segundo bot\u00e3o", + "button_3": "Terceiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o", + "button_5": "Quinto bot\u00e3o", + "button_6": "Sexto bot\u00e3o", + "close": "Fechado", + "dim_down": "Diminuir a luminosidade", + "dim_up": "Aumentar a luminosidade", + "face_1": "com face 1 ativada", + "face_2": "com face 2 ativada", + "face_3": "com face 3 ativada", + "face_4": "com face 4 ativada", + "face_5": "com face 5 ativada", + "face_6": "com face 6 ativada", + "face_any": "Com qualquer face(s) especificada(s) ativada(s)", + "left": "Esquerdo", + "open": "Aberto", + "right": "Direito", + "turn_off": "Desligar", + "turn_on": "Ligar" }, "trigger_type": { - "device_offline": "Dispositivo offline" + "device_dropped": "Dispositivo caiu", + "device_flipped": "Dispositivo invertido \" {subtype} \"", + "device_knocked": "Dispositivo batido \" {subtype} \"", + "device_offline": "Dispositivo offline", + "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_shaken": "Dispositivo sacudido", + "device_slid": "Dispositivo deslizou \" {subtype} \"", + "device_tilted": "Dispositivo inclinado", + "remote_button_alt_double_press": "Bot\u00e3o \" {subtype} \" clicado duas vezes (modo alternativo)", + "remote_button_alt_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente (modo alternativo)", + "remote_button_alt_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa (modo alternativo)", + "remote_button_alt_quadruple_press": "Bot\u00e3o \" {subtype} \" clicado quatro vezes (modo alternativo)", + "remote_button_alt_quintuple_press": "Bot\u00e3o \" {subtype} \" clicado qu\u00edntuplo (modo alternativo)", + "remote_button_alt_short_press": "Bot\u00e3o \" {subtype} \" pressionado (modo alternativo)", + "remote_button_alt_short_release": "Bot\u00e3o \" {subtype} \" liberado (modo alternativo)", + "remote_button_alt_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes (modo alternativo)", + "remote_button_double_press": "bot\u00e3o \" {subtype} \" clicado duas vezes", + "remote_button_long_press": "Bot\u00e3o \" {subtype} \" pressionado continuamente", + "remote_button_long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", + "remote_button_quadruple_press": "Bot\u00e3o \" {subtype} \" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "Bot\u00e3o \" {subtype} \" qu\u00edntuplo clicado", + "remote_button_short_press": "Bot\u00e3o \" {subtype} \" pressionado", + "remote_button_short_release": "Bot\u00e3o \" {subtype} \" liberado", + "remote_button_triple_press": "Bot\u00e3o \" {subtype} \" clicado tr\u00eas vezes" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json index 7bd62cf26e1dc..f7206911534ef 100644 --- a/homeassistant/components/zha/translations/uk.json +++ b/homeassistant/components/zha/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 28b8f70a8ddda..e0904cf06830f 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_zha_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e ZHA \u88dd\u7f6e", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "usb_probe_failed": "\u5075\u6e2c USB \u88dd\u7f6e\u5931\u6557" }, "error": { diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index c57e23507c97e..d953675965f9d 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/zhong_hong", "requirements": ["zhong_hong_hvac==1.0.9"], "codeowners": [], - "iot_class": "local_push" + "iot_class": "local_push", + "loggers": ["zhong_hong_hvac"] } diff --git a/homeassistant/components/zodiac/translations/sensor.pt-BR.json b/homeassistant/components/zodiac/translations/sensor.pt-BR.json new file mode 100644 index 0000000000000..9662e6160e435 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.pt-BR.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aqu\u00e1rio", + "aries": "\u00c1ries", + "cancer": "C\u00e2ncer", + "capricorn": "Capric\u00f3rnio", + "gemini": "G\u00eameos", + "leo": "Le\u00e3o", + "libra": "Libra", + "pisces": "Peixes", + "sagittarius": "Sagit\u00e1rio", + "scorpio": "Escorpi\u00e3o", + "taurus": "Touro", + "virgo": "Virgem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index dd327acbf7582..ef2d21281d19e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -122,7 +122,7 @@ def async_active_zone( continue within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] - closer_zone = closest is None or zone_dist < min_dist # type: ignore + closer_zone = closest is None or zone_dist < min_dist # type: ignore[unreachable] smaller_zone = ( zone_dist == min_dist and zone.attributes[ATTR_RADIUS] diff --git a/homeassistant/components/zone/translations/el.json b/homeassistant/components/zone/translations/el.json index c71e66f6434af..1d8e5a1c16327 100644 --- a/homeassistant/components/zone/translations/el.json +++ b/homeassistant/components/zone/translations/el.json @@ -6,9 +6,16 @@ "step": { "init": { "data": { - "icon": "\u0395\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf" - } + "icon": "\u0395\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03cc", + "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1" + }, + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b6\u03ce\u03bd\u03b7\u03c2" } - } + }, + "title": "\u0396\u03ce\u03bd\u03b7" } } \ No newline at end of file diff --git a/homeassistant/components/zone/translations/sk.json b/homeassistant/components/zone/translations/sk.json new file mode 100644 index 0000000000000..5272ec1315a20 --- /dev/null +++ b/homeassistant/components/zone/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index a008a30007abe..5a11bf2068dcc 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -79,8 +79,7 @@ def zone_automation_listener(zone_event): ): return - zone_state = hass.states.get(zone_entity_id) - if not zone_state: + if not (zone_state := hass.states.get(zone_entity_id)): _LOGGER.warning( "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", automation_info["name"], diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 70e9548414eed..a7f0cf1d7fae5 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -3,13 +3,7 @@ import logging -from homeassistant.components.mjpeg.camera import ( - CONF_MJPEG_URL, - CONF_STILL_IMAGE_URL, - MjpegCamera, - filter_urllib3_logging, -) -from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL +from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,13 +38,12 @@ class ZoneMinderCamera(MjpegCamera): def __init__(self, monitor, verify_ssl): """Initialize as a subclass of MjpegCamera.""" - device_info = { - CONF_NAME: monitor.name, - CONF_MJPEG_URL: monitor.mjpeg_image_url, - CONF_STILL_IMAGE_URL: monitor.still_image_url, - CONF_VERIFY_SSL: verify_ssl, - } - super().__init__(device_info) + super().__init__( + name=monitor.name, + mjpeg_url=monitor.mjpeg_image_url, + still_image_url=monitor.still_image_url, + verify_ssl=verify_ssl, + ) self._is_recording = None self._is_available = None self._monitor = monitor diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 92324f338b552..699e2e5b7a4fc 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": ["zm-py==0.5.2"], "codeowners": ["@rohankapoorcom"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "loggers": ["zoneminder"] } diff --git a/homeassistant/components/zoneminder/translations/el.json b/homeassistant/components/zoneminder/translations/el.json index 8151193538607..c370d342e1f3a 100644 --- a/homeassistant/components/zoneminder/translations/el.json +++ b/homeassistant/components/zoneminder/translations/el.json @@ -2,14 +2,18 @@ "config": { "abort": { "auth_fail": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1.", - "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder." + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder.", + "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" }, "create_entry": { "default": "\u03a0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 ZoneMinder." }, "error": { "auth_fail": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b1.", - "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder." + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae ZoneMinder.", + "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" }, "flow_title": "ZoneMinder", "step": { diff --git a/homeassistant/components/zoneminder/translations/pt-BR.json b/homeassistant/components/zoneminder/translations/pt-BR.json new file mode 100644 index 0000000000000..a7fada70a8336 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "auth_fail": "Nome de usu\u00e1rio ou senha est\u00e1 incorreta.", + "cannot_connect": "Falha ao conectar", + "connection_error": "Falha ao conectar a um servidor ZoneMinder.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "create_entry": { + "default": "Servidor ZoneMinder adicionado." + }, + "error": { + "auth_fail": "Nome de usu\u00e1rio ou senha est\u00e1 incorreta.", + "cannot_connect": "Falha ao conectar", + "connection_error": "Falha ao conectar a um servidor ZoneMinder.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Host e Porta (ex 10.10.0.4:8010)", + "password": "Senha", + "path": "Caminho ZM", + "path_zms": "Caminho ZMS", + "ssl": "Usar um certificado SSL", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + }, + "title": "Adicione o Servidor ZoneMinder." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/sk.json b/homeassistant/components/zoneminder/translations/sk.json new file mode 100644 index 0000000000000..2c3ed1dd93049 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/sk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index cd0bda6735ff2..3424aa11a87e8 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -1,4 +1,5 @@ """Support for Z-Wave.""" +# pylint: disable=import-error # pylint: disable=import-outside-toplevel from __future__ import annotations @@ -355,8 +356,6 @@ async def async_setup_entry( # noqa: C901 from openzwave.group import ZWaveGroup from openzwave.network import ZWaveNetwork from openzwave.option import ZWaveOption - - # pylint: enable=import-error from pydispatch import dispatcher if async_is_ozw_migrated(hass) or async_is_zwave_js_migrated(hass): diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index ce7aebd801a13..f29f2e6f6d096 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Z-Wave.""" +# pylint: disable=import-error # pylint: disable=import-outside-toplevel from collections import OrderedDict diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index b17034e0e8a7a..ade6828431326 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -1,4 +1,5 @@ """Entity class that represents Z-Wave node.""" +# pylint: disable=import-error # pylint: disable=import-outside-toplevel from itertools import count diff --git a/homeassistant/components/zwave/translations/el.json b/homeassistant/components/zwave/translations/el.json index 1663d4975a9f6..ca7149b26b51a 100644 --- a/homeassistant/components/zwave/translations/el.json +++ b/homeassistant/components/zwave/translations/el.json @@ -1,9 +1,17 @@ { "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", + "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": { + "option_error": "\u0397 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 Z-Wave \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u0395\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c3\u03c4\u03b9\u03ba\u03ac\u03ba\u03b9 USB;" + }, "step": { "user": { "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)" + "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" }, "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u0393\u03b9\u03b1 \u03bd\u03ad\u03b5\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Z-Wave JS. \n\n \u0394\u03b5\u03af\u03c4\u03b5 https://www.home-assistant.io/docs/z-wave/installation/ \u03b3\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ad\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2" } diff --git a/homeassistant/components/zwave/translations/pt-BR.json b/homeassistant/components/zwave/translations/pt-BR.json index 8c20db1383085..079f1ab85932d 100644 --- a/homeassistant/components/zwave/translations/pt-BR.json +++ b/homeassistant/components/zwave/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Z-Wave j\u00e1 est\u00e1 configurado." + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o USB est\u00e1 correto?" @@ -10,7 +11,7 @@ "user": { "data": { "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", - "usb_path": "Caminho do USB" + "usb_path": "Caminho do Dispositivo USB" }, "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" } @@ -24,8 +25,8 @@ "sleeping": "Dormindo" }, "query_stage": { - "dead": "Morto ({query_stage})", - "initializing": "Iniciando ( {query_stage} )" + "dead": "Morto", + "initializing": "Iniciando" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sk.json b/homeassistant/components/zwave/translations/sk.json index f53db0f9721a7..9819295ee1f74 100644 --- a/homeassistant/components/zwave/translations/sk.json +++ b/homeassistant/components/zwave/translations/sk.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + } + }, "state": { "_": { "dead": "Nereaguje", diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json index 696c0caccd2ba..3c5b49681c84a 100644 --- a/homeassistant/components/zwave/translations/uk.json +++ b/homeassistant/components/zwave/translations/uk.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + "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." }, "error": { "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index f7979daff9e4f..9dc8810f4999f 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 10088f6241435..5d294931e6626 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -12,9 +12,11 @@ from zwave_js_server.model.notification import ( EntryControlNotification, NotificationNotification, + PowerLevelNotification, ) from zwave_js_server.model.value import Value, ValueNotification +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -40,6 +42,7 @@ from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api from .const import ( + ATTR_ACKNOWLEDGED_FRAMES, ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DATA_TYPE, @@ -56,6 +59,8 @@ ATTR_PROPERTY_KEY, ATTR_PROPERTY_KEY_NAME, ATTR_PROPERTY_NAME, + ATTR_STATUS, + ATTR_TEST_NODE_ID, ATTR_TYPE, ATTR_VALUE, ATTR_VALUE_RAW, @@ -93,6 +98,7 @@ get_device_id, get_device_id_ext, get_unique_id, + get_valueless_base_unique_id, ) from .migrate import async_migrate_discovered_value from .services import ZWaveServices @@ -171,11 +177,19 @@ async def async_setup_entry( # noqa: C901 entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_PLATFORM_SETUP] = {} + platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] = {} registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) discovered_value_ids: dict[str, set[str]] = defaultdict(set) + async def async_setup_platform(platform: str) -> None: + """Set up platform if needed.""" + if platform not in platform_setup_tasks: + platform_setup_tasks[platform] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + await platform_setup_tasks[platform] + @callback def remove_device(device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" @@ -202,13 +216,8 @@ async def async_handle_discovery_info( disc_info, ) - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] platform = disc_info.platform - if platform not in platform_setup_tasks: - platform_setup_tasks[platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - await platform_setup_tasks[platform] + await async_setup_platform(platform) LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( @@ -284,24 +293,19 @@ async def async_on_node_ready(node: ZwaveNode) -> None: async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] - - # We need to set up the sensor platform if it hasn't already been setup in - # order to create the node status sensor - if SENSOR_DOMAIN not in platform_setup_tasks: - platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await async_setup_platform(SENSOR_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node ) - # This guard ensures that concurrent runs of this function all await the - # platform setup task - if not platform_setup_tasks[SENSOR_DOMAIN].done(): - await platform_setup_tasks[SENSOR_DOMAIN] - - # Create a node status sensor for each device - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) + # Create a ping button for each device + await async_setup_platform(BUTTON_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node + ) # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. @@ -358,7 +362,7 @@ def async_on_node_removed(event: dict) -> None: async_dispatcher_send( hass, - f"{DOMAIN}_{client.driver.controller.home_id}.{node.node_id}.node_status_remove_entity", + f"{DOMAIN}_{get_valueless_base_unique_id(client, node)}_remove_entity", ) else: remove_device(device) @@ -394,7 +398,9 @@ def async_on_value_notification(notification: ValueNotification) -> None: @callback def async_on_notification( - notification: EntryControlNotification | NotificationNotification, + notification: EntryControlNotification + | NotificationNotification + | PowerLevelNotification, ) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device({get_device_id(client, notification.node)}) @@ -417,7 +423,7 @@ def async_on_notification( ATTR_EVENT_DATA: notification.event_data, } ) - else: + elif isinstance(notification, NotificationNotification): event_data.update( { ATTR_COMMAND_CLASS_NAME: "Notification", @@ -428,6 +434,17 @@ def async_on_notification( ATTR_PARAMETERS: notification.parameters, } ) + elif isinstance(notification, PowerLevelNotification): + event_data.update( + { + ATTR_COMMAND_CLASS_NAME: "Power Level", + ATTR_TEST_NODE_ID: notification.test_node_id, + ATTR_STATUS: notification.status, + ATTR_ACKNOWLEDGED_FRAMES: notification.acknowledged_frames, + } + ) + else: + raise TypeError(f"Unhandled notification type: {notification}") hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) @@ -446,9 +463,7 @@ def async_on_value_updated_fire_event( # We assert because we know the device exists assert device - unique_id = get_unique_id( - client.driver.controller.home_id, disc_info.primary_value.value_id - ) + unique_id = get_unique_id(client, disc_info.primary_value.value_id) entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) raw_value = value_ = value.value diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6208091dd8d4e..4e68cc2e2dd1c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -367,6 +367,7 @@ async def websocket_network_status( ) -> None: """Get the status of the Z-Wave JS network.""" controller = client.driver.controller + await controller.async_get_state() data = { "client": { "ws_server_url": client.ws_server_url, @@ -393,6 +394,7 @@ async def websocket_network_status( "suc_node_id": controller.suc_node_id, "supports_timers": controller.supports_timers, "is_heal_network_active": controller.is_heal_network_active, + "inclusion_state": controller.inclusion_state, "nodes": list(client.driver.controller.nodes), }, } @@ -462,6 +464,7 @@ async def websocket_node_status( "ready": node.ready, "zwave_plus_version": node.zwave_plus_version, "highest_security_class": node.highest_security_class, + "is_controller_node": node.is_controller_node, } connection.send_result( msg[ID], diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py new file mode 100644 index 0000000000000..fb62bbdc3dc16 --- /dev/null +++ b/homeassistant/components/zwave_js/button.py @@ -0,0 +1,89 @@ +"""Representation of Z-Wave buttons.""" +from __future__ import annotations + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode + +from homeassistant.components.button import ButtonEntity +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, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_CLIENT, DOMAIN, LOGGER +from .helpers import get_device_id, get_valueless_base_unique_id + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave button from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_ping_button_entity(node: ZwaveNode) -> None: + """Add ping button entity.""" + async_add_entities([ZWaveNodePingButton(client, node)]) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_ping_button_entity", + async_add_ping_button_entity, + ) + ) + + +class ZWaveNodePingButton(ButtonEntity): + """Representation of a ping button entity.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, client: ZwaveClient, node: ZwaveNode) -> None: + """Initialize a ping Z-Wave device button entity.""" + self.node = node + name: str = ( + node.name or node.device_config.description or f"Node {node.node_id}" + ) + # Entity class attributes + self._attr_name = f"{name}: Ping" + self._base_unique_id = get_valueless_base_unique_id(client, node) + self._attr_unique_id = f"{self._base_unique_id}.ping" + # device is precreated in main handler + self._attr_device_info = DeviceInfo( + identifiers={get_device_id(client, node)}, + ) + + async def async_poll_value(self, _: bool) -> None: + """Poll a value.""" + # pylint: disable=no-self-use + LOGGER.error( + "There is no value to refresh for this entity so the zwave_js.refresh_value " + "service won't work for it" + ) + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_poll_value", + self.async_poll_value, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._base_unique_id}_remove_entity", + self.async_remove, + ) + ) + + async def async_press(self) -> None: + """Press the button.""" + self.hass.async_create_task(self.node.async_ping()) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 6df95d9bbfce0..88c96feea88bf 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -242,7 +242,7 @@ def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType | None]: if self._current_mode is None: # Thermostat(valve) with no support for setting a mode is considered heating-only return [ThermostatSetpointType.HEATING] - return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore + return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore[no-any-return] @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 2d16c0113c926..8f6fada2106ac 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -56,6 +56,9 @@ ATTR_DATA_TYPE = "data_type" ATTR_WAIT_FOR_RESULT = "wait_for_result" ATTR_OPTIONS = "options" +ATTR_TEST_NODE_ID = "test_node_id" +ATTR_STATUS = "status" +ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames" ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" @@ -66,6 +69,9 @@ ATTR_CURRENT_VALUE = "current_value" ATTR_CURRENT_VALUE_RAW = "current_value_raw" ATTR_DESCRIPTION = "description" +ATTR_EVENT_SOURCE = "event_source" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" +ATTR_PARTIAL_DICT_MATCH = "partial_dict_match" # service constants SERVICE_SET_LOCK_USERCODE = "set_lock_usercode" diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 9f6fa7fc35cf7..b81d675e6fd72 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -53,6 +53,7 @@ from .device_automation_helpers import ( CONF_SUBTYPE, VALUE_ID_REGEX, + generate_config_parameter_subtype, get_config_parameter_value_schema, ) from .helpers import async_get_node_from_device_id @@ -165,7 +166,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: CONF_TYPE: SERVICE_SET_CONFIG_PARAMETER, ATTR_CONFIG_PARAMETER: config_value.property_, ATTR_CONFIG_PARAMETER_BITMASK: config_value.property_key, - CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})", + CONF_SUBTYPE: generate_config_parameter_subtype(config_value), } for config_value in node.get_configuration_values().values() ] diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index cfdb65a4b028c..906efb2c4f951 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -32,3 +32,12 @@ def get_config_parameter_value_schema(node: Node, value_id: str) -> vol.Schema | return vol.In({int(k): v for k, v in config_value.metadata.states.items()}) return None + + +def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: + """Generate the config parameter name used in a device automation subtype.""" + parameter = str(config_value.property_) + if config_value.property_key: + parameter = f"{parameter}[{hex(config_value.property_key)}]" + + return f"{parameter} ({config_value.property_name})" diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index fcd769dc8a4a2..8bb151199d774 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -29,6 +29,7 @@ CONF_SUBTYPE, CONF_VALUE_ID, NODE_STATUSES, + generate_config_parameter_subtype, get_config_parameter_value_schema, ) from .helpers import ( @@ -146,7 +147,7 @@ async def async_get_conditions( **base_condition, CONF_VALUE_ID: config_value.value_id, CONF_TYPE: CONFIG_PARAMETER_TYPE, - CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})", + CONF_SUBTYPE: generate_config_parameter_subtype(config_value), } for config_value in node.get_configuration_values().values() ] diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 888efbf2bfd19..0615668cccdd4 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -49,7 +49,11 @@ ZWAVE_JS_NOTIFICATION_EVENT, ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ) -from .device_automation_helpers import CONF_SUBTYPE, NODE_STATUSES +from .device_automation_helpers import ( + CONF_SUBTYPE, + NODE_STATUSES, + generate_config_parameter_subtype, +) from .helpers import ( async_get_node_from_device_id, async_get_node_status_sensor_entity_id, @@ -353,7 +357,7 @@ async def async_get_triggers( ATTR_PROPERTY_KEY: config_value.property_key, ATTR_ENDPOINT: config_value.endpoint, ATTR_COMMAND_CLASS: config_value.command_class, - CONF_SUBTYPE: f"{config_value.value_id} ({config_value.property_name})", + CONF_SUBTYPE: generate_config_parameter_subtype(config_value), } for config_value in node.get_configuration_values().values() ] diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 080fffe2107fd..8b59c38d405b5 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntry from .const import DATA_CLIENT, DOMAIN from .helpers import get_home_and_node_id_from_device_entry @@ -26,7 +26,7 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry ) -> NodeDataType: """Return diagnostics for a device.""" client: Client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -42,8 +42,5 @@ async def async_get_device_diagnostics( "minSchemaVersion": client.version.min_schema_version, "maxSchemaVersion": client.version.max_schema_version, }, - "state": { - **node.data, - "values": [value.data for value in node.values.values()], - }, + "state": node.data, } diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 3692e50d595a1..69a3d05539b7c 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -16,6 +16,10 @@ from zwave_js_server.const.command_class.barrier_operator import ( SIGNALING_STATE_PROPERTY, ) +from zwave_js_server.const.command_class.color_switch import CURRENT_COLOR_PROPERTY +from zwave_js_server.const.command_class.humidity_control import ( + HUMIDITY_CONTROL_MODE_PROPERTY, +) from zwave_js_server.const.command_class.lock import ( CURRENT_MODE_PROPERTY, DOOR_STATUS_PROPERTY, @@ -122,9 +126,9 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne): # [optional] the value's property name must match ANY of these values property_name: set[str] | None = None # [optional] the value's property key must match ANY of these values - property_key: set[str | int] | None = None + property_key: set[str | int | None] | None = None # [optional] the value's property key name must match ANY of these values - property_key_name: set[str] | None = None + property_key_name: set[str | None] | None = None # [optional] the value's metadata_type must match ANY of these values type: set[str] | None = None @@ -177,8 +181,8 @@ class ZWaveDiscoverySchema: def get_config_parameter_discovery_schema( property_: set[str | int] | None = None, property_name: set[str] | None = None, - property_key: set[str | int] | None = None, - property_key_name: set[str] | None = None, + property_key: set[str | int | None] | None = None, + property_key_name: set[str | None] | None = None, **kwargs: Any, ) -> ZWaveDiscoverySchema: """ @@ -459,6 +463,20 @@ def get_config_parameter_discovery_schema( }, ), ), + # HomeSeer HSM-200 v1 + ZWaveDiscoverySchema( + platform="light", + hint="black_is_off", + manufacturer_id={0x001E}, + product_id={0x0001}, + product_type={0x0004}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_COLOR}, + property={CURRENT_COLOR_PROPERTY}, + property_key={None}, + ), + absent_values=[SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA], + ), # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= # Door lock mode config parameter. Functionality equivalent to Notification CC # list sensors. @@ -492,6 +510,16 @@ def get_config_parameter_discovery_schema( type={"any"}, ), ), + # humidifier + # hygrostats supporting mode (and optional setpoint) + ZWaveDiscoverySchema( + platform="humidifier", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.HUMIDITY_CONTROL_MODE}, + property={HUMIDITY_CONTROL_MODE_PROPERTY}, + type={"number"}, + ), + ), # climate # thermostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 87e9f3adbbd09..a61fc3765c7cc 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -48,7 +48,7 @@ def __init__( # Entity class attributes self._attr_name = self.generate_name() self._attr_unique_id = get_unique_id( - self.client.driver.controller.home_id, self.info.primary_value.value_id + self.client, self.info.primary_value.value_id ) self._attr_entity_registry_enabled_default = ( self.info.entity_registry_enabled_default diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index de7ed5da50213..05df480a4876e 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -55,9 +55,14 @@ def update_data_collection_preference( @callback -def get_unique_id(home_id: str, value_id: str) -> str: - """Get unique ID from home ID and value ID.""" - return f"{home_id}.{value_id}" +def get_valueless_base_unique_id(client: ZwaveClient, node: ZwaveNode) -> str: + """Return the base unique ID for an entity that is not based on a value.""" + return f"{client.driver.controller.home_id}.{node.node_id}" + + +def get_unique_id(client: ZwaveClient, value_id: str) -> str: + """Get unique ID from client and value ID.""" + return f"{client.driver.controller.home_id}.{value_id}" @callback @@ -297,8 +302,7 @@ def async_is_device_config_entry_not_loaded( ) -> bool: """Return whether device's config entries are not loaded.""" dev_reg = dr.async_get(hass) - device = dev_reg.async_get(device_id) - if device is None: + if (device := dev_reg.async_get(device_id)) is None: raise ValueError(f"Device {device_id} not found") return any( (entry := hass.config_entries.async_get_entry(entry_id)) diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py new file mode 100644 index 0000000000000..b94c7a8e2a362 --- /dev/null +++ b/homeassistant/components/zwave_js/humidifier.py @@ -0,0 +1,217 @@ +"""Representation of Z-Wave humidifiers.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.humidity_control import ( + HUMIDITY_CONTROL_SETPOINT_PROPERTY, + HumidityControlMode, + HumidityControlSetpointType, +) +from zwave_js_server.model.value import Value as ZwaveValue + +from homeassistant.components.humidifier import ( + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityDescription, +) +from homeassistant.components.humidifier.const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DOMAIN as HUMIDIFIER_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_CLIENT, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +@dataclass +class ZwaveHumidifierEntityDescriptionRequiredKeys: + """A class for humidifier entity description required keys.""" + + # The "on" control mode for this entity, e.g. HUMIDIFY for humidifier + on_mode: HumidityControlMode + + # The "on" control mode for the inverse entity, e.g. DEHUMIDIFY for humidifier + inverse_mode: HumidityControlMode + + # The setpoint type controlled by this entity + setpoint_type: HumidityControlSetpointType + + +@dataclass +class ZwaveHumidifierEntityDescription( + HumidifierEntityDescription, ZwaveHumidifierEntityDescriptionRequiredKeys +): + """A class that describes the humidifier or dehumidifier entity.""" + + +HUMIDIFIER_ENTITY_DESCRIPTION = ZwaveHumidifierEntityDescription( + key="humidifier", + device_class=HumidifierDeviceClass.HUMIDIFIER, + on_mode=HumidityControlMode.HUMIDIFY, + inverse_mode=HumidityControlMode.DEHUMIDIFY, + setpoint_type=HumidityControlSetpointType.HUMIDIFIER, +) + + +DEHUMIDIFIER_ENTITY_DESCRIPTION = ZwaveHumidifierEntityDescription( + key="dehumidifier", + device_class=HumidifierDeviceClass.DEHUMIDIFIER, + on_mode=HumidityControlMode.DEHUMIDIFY, + inverse_mode=HumidityControlMode.HUMIDIFY, + setpoint_type=HumidityControlSetpointType.DEHUMIDIFIER, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Z-Wave humidifier from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_humidifier(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Humidifier.""" + entities: list[ZWaveBaseEntity] = [] + + if ( + str(HumidityControlMode.HUMIDIFY.value) + in info.primary_value.metadata.states + ): + entities.append( + ZWaveHumidifier( + config_entry, client, info, HUMIDIFIER_ENTITY_DESCRIPTION + ) + ) + + if ( + str(HumidityControlMode.DEHUMIDIFY.value) + in info.primary_value.metadata.states + ): + entities.append( + ZWaveHumidifier( + config_entry, client, info, DEHUMIDIFIER_ENTITY_DESCRIPTION + ) + ) + + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{HUMIDIFIER_DOMAIN}", + async_add_humidifier, + ) + ) + + +class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): + """Representation of a Z-Wave Humidifier or Dehumidifier.""" + + entity_description: ZwaveHumidifierEntityDescription + _current_mode: ZwaveValue + _setpoint: ZwaveValue | None = None + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + description: ZwaveHumidifierEntityDescription, + ) -> None: + """Initialize humidifier.""" + super().__init__(config_entry, client, info) + + self.entity_description = description + + self._attr_name = f"{self._attr_name} {description.key}" + self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" + + self._current_mode = self.info.primary_value + + self._setpoint = self.get_zwave_value( + HUMIDITY_CONTROL_SETPOINT_PROPERTY, + command_class=CommandClass.HUMIDITY_CONTROL_SETPOINT, + value_property_key=description.setpoint_type, + add_to_watched_value_ids=True, + ) + + @property + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return int(self._current_mode.value) in [ + self.entity_description.on_mode, + HumidityControlMode.AUTO, + ] + + def _supports_inverse_mode(self) -> bool: + return ( + str(self.entity_description.inverse_mode.value) + in self._current_mode.metadata.states + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on device.""" + mode = int(self._current_mode.value) + if mode == HumidityControlMode.OFF: + new_mode = self.entity_description.on_mode + elif mode == self.entity_description.inverse_mode: + new_mode = HumidityControlMode.AUTO + else: + return + + await self.info.node.async_set_value(self._current_mode, new_mode) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off device.""" + mode = int(self._current_mode.value) + if mode == HumidityControlMode.AUTO: + if self._supports_inverse_mode(): + new_mode = self.entity_description.inverse_mode + else: + new_mode = HumidityControlMode.OFF + elif mode == self.entity_description.on_mode: + new_mode = HumidityControlMode.OFF + else: + return + + await self.info.node.async_set_value(self._current_mode, new_mode) + + @property + def target_humidity(self) -> int | None: + """Return the humidity we try to reach.""" + if not self._setpoint: + return None + return int(self._setpoint.value) + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + if self._setpoint: + await self.info.node.async_set_value(self._setpoint, humidity) + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + min_value = DEFAULT_MIN_HUMIDITY + if self._setpoint and self._setpoint.metadata.min: + min_value = self._setpoint.metadata.min + return min_value + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + max_value = DEFAULT_MAX_HUMIDITY + if self._setpoint and self._setpoint.metadata.max: + max_value = self._setpoint.metadata.max + return max_value diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index caba0f5de36d6..0f3f17df41a47 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -74,8 +74,10 @@ async def async_setup_entry( def async_add_light(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Light.""" - light = ZwaveLight(config_entry, client, info) - async_add_entities([light]) + if info.platform_hint == "black_is_off": + async_add_entities([ZwaveBlackIsOffLight(config_entry, client, info)]) + else: + async_add_entities([ZwaveLight(config_entry, client, info)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -127,7 +129,9 @@ def __init__( # get additional (optional) values and set features self._target_brightness = self.get_zwave_value( - TARGET_VALUE_PROPERTY, add_to_watched_value_ids=False + TARGET_VALUE_PROPERTY, + CommandClass.SWITCH_MULTILEVEL, + add_to_watched_value_ids=False, ) self._target_color = self.get_zwave_value( TARGET_COLOR_PROPERTY, @@ -167,14 +171,14 @@ def on_value_update(self) -> None: self._calculate_color_values() @property - def brightness(self) -> int: + def brightness(self) -> int | None: """Return the brightness of this light between 0..255. Z-Wave multilevel switches use a range of [0, 99] to control brightness. """ - if self.info.primary_value.value is not None: - return round((self.info.primary_value.value / 99) * 255) - return 0 + if self.info.primary_value.value is None: + return None + return round((self.info.primary_value.value / 99) * 255) @property def color_mode(self) -> str | None: @@ -182,9 +186,12 @@ def color_mode(self) -> str | None: return self._color_mode @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if device is on (brightness above 0).""" - return self.brightness > 0 + brightness = self.brightness + if brightness is None: + return None + return brightness > 0 @property def hs_color(self) -> tuple[float, float] | None: @@ -318,6 +325,9 @@ async def _async_set_brightness( self, brightness: int | None, transition: float | None = None ) -> None: """Set new brightness to light.""" + # If we have no target brightness value, there is nothing to do + if not self._target_brightness: + return if brightness is None: # Level 255 means to set it to previous value. zwave_brightness = 255 @@ -426,3 +436,77 @@ def _calculate_color_values(self) -> None: self._rgbw_color = (red, green, blue, white) # Light supports rgbw, set color mode to rgbw self._color_mode = COLOR_MODE_RGBW + + +class ZwaveBlackIsOffLight(ZwaveLight): + """ + Representation of a Z-Wave light where setting the color to black turns it off. + + Currently only supports lights with RGB, no color temperature, and no white channels. + """ + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the light.""" + super().__init__(config_entry, client, info) + + self._last_color: dict[str, int] | None = None + self._supported_color_modes.discard(COLOR_MODE_BRIGHTNESS) + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return 255 + + @property + def is_on(self) -> bool | None: + """Return true if device is on (brightness above 0).""" + if self.info.primary_value.value is None: + return None + return any(value != 0 for value in self.info.primary_value.value.values()) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await super().async_turn_on(**kwargs) + + if ( + kwargs.get(ATTR_RGBW_COLOR) is not None + or kwargs.get(ATTR_COLOR_TEMP) is not None + or kwargs.get(ATTR_HS_COLOR) is not None + ): + return + + transition = kwargs.get(ATTR_TRANSITION) + # turn on light to last color if known, otherwise set to white + if self._last_color is not None: + await self._async_set_colors( + { + ColorComponent.RED: self._last_color["red"], + ColorComponent.GREEN: self._last_color["green"], + ColorComponent.BLUE: self._last_color["blue"], + }, + transition, + ) + else: + await self._async_set_colors( + { + ColorComponent.RED: 255, + ColorComponent.GREEN: 255, + ColorComponent.BLUE: 255, + }, + transition, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + self._last_color = self.info.primary_value.value + await self._async_set_colors( + { + ColorComponent.RED: 0, + ColorComponent.GREEN: 0, + ColorComponent.BLUE: 0, + }, + kwargs.get(ATTR_TRANSITION), + ) + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index f56255c736dc4..6a892b2791d8a 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.34.0"], + "requirements": ["zwave-js-server-python==0.35.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", @@ -11,5 +11,6 @@ {"vid":"0658","pid":"0200","known_devices":["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"]}, {"vid":"10C4","pid":"8A2A","description":"*z-wave*","known_devices":["Nortek HUSBZB-1"]}, {"vid":"10C4","pid":"EA60","known_devices":["Aeotec Z-Stick 7", "Silicon Labs UZB-7", "Zooz ZST10 700"]} - ] + ], + "loggers": ["zwave_js_server"] } diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index d9b46ac725c0d..204b5d0aebd5f 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -9,7 +9,7 @@ from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import LIGHT_LUX, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -91,6 +91,8 @@ 113: NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME, } +UNIT_LEGACY_MIGRATION_MAP = {LIGHT_LUX: "Lux"} + class ZWaveMigrationData(TypedDict): """Represent the Z-Wave migration data dict.""" @@ -209,7 +211,8 @@ def add_entity_value( # Normalize unit of measurement. if unit := entity_entry.unit_of_measurement: - unit = unit.lower() + _unit = UNIT_LEGACY_MIGRATION_MAP.get(unit, unit) + unit = _unit.lower() if unit == "": unit = None @@ -470,10 +473,7 @@ def async_migrate_discovered_value( ) -> None: """Migrate unique ID for entity/entities tied to discovered value.""" - new_unique_id = get_unique_id( - client.driver.controller.home_id, - disc_info.primary_value.value_id, - ) + new_unique_id = get_unique_id(client, disc_info.primary_value.value_id) # On reinterviews, there is no point in going through this logic again for already # discovered values @@ -485,10 +485,7 @@ def async_migrate_discovered_value( # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats old_unique_ids = [ - get_unique_id( - client.driver.controller.home_id, - value_id, - ) + get_unique_id(client, value_id) for value_id in get_old_value_ids(disc_info.primary_value) ] diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 76cb6fd22e911..8ac909d76de9a 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -61,7 +61,7 @@ NumericSensorDataTemplateData, ) from .entity import ZWaveBaseEntity -from .helpers import get_device_id +from .helpers import get_device_id, get_valueless_base_unique_id LOGGER = logging.getLogger(__name__) @@ -477,9 +477,8 @@ def __init__( ) # Entity class attributes self._attr_name = f"{name}: Node Status" - self._attr_unique_id = ( - f"{self.client.driver.controller.home_id}.{node.node_id}.node_status" - ) + self._base_unique_id = get_valueless_base_unique_id(client, node) + self._attr_unique_id = f"{self._base_unique_id}.node_status" # device is precreated in main handler self._attr_device_info = DeviceInfo( identifiers={get_device_id(self.client, self.node)}, @@ -489,7 +488,10 @@ def __init__( async def async_poll_value(self, _: bool) -> None: """Poll a value.""" # pylint: disable=no-self-use - raise ValueError("There is no value to poll for this entity") + LOGGER.error( + "There is no value to refresh for this entity so the zwave_js.refresh_value " + "service won't work for it" + ) @callback def _status_changed(self, _: dict) -> None: @@ -517,7 +519,7 @@ async def async_added_to_hass(self) -> None: self.async_on_remove( async_dispatcher_connect( self.hass, - f"{DOMAIN}_{self.unique_id}_remove_entity", + f"{DOMAIN}_{self._base_unique_id}_remove_entity", self.async_remove, ) ) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index ac3f233ba4959..767516cc17c01 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -457,7 +457,7 @@ async def async_multicast_set_value(self, service: ServiceCall) -> None: options = service.data.get(const.ATTR_OPTIONS) if not broadcast and len(nodes) == 1: - const.LOGGER.warning( + const.LOGGER.info( "Passing the zwave_js.multicast_set_value service call to the " "zwave_js.set_value service since only one node was targeted" ) @@ -520,5 +520,10 @@ async def async_multicast_set_value(self, service: ServiceCall) -> None: async def async_ping(self, service: ServiceCall) -> None: """Ping node(s).""" # pylint: disable=no-self-use + const.LOGGER.warning( + "This service is deprecated in favor of the ping button entity. Service " + "calls will still work for now but the service will be removed in a " + "future release" + ) nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] await asyncio.gather(*(node.async_ping() for node in nodes)) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index b41a893c7e4e5..206af776a6107 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -176,7 +176,7 @@ multicast_set_value: fields: broadcast: name: Broadcast? - description: Whether command should be broadcast to all devices on the networrk. + description: Whether command should be broadcast to all devices on the network. example: true required: false selector: diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 56683e692c9e3..2a4a519369a87 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -1,15 +1,59 @@ { "config": { "abort": { + "addon_get_discovery_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 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", + "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 Z-Wave JS.", + "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 Z-Wave JS.", + "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 Z-Wave JS.", "addon_start_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", + "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", + "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." }, + "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.", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_ws_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL websocket", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "flow_title": "{name}", "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 Z-Wave JS. \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\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 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS. \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": { + "configure_addon": { + "data": { + "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "s0_legacy_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af S0 (\u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5)", + "s2_access_control_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 S2", + "s2_authenticated_key": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", + "s2_unauthenticated_key": "\u039c\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03c0\u03b5\u03b4\u03af\u03b1 \u03b1\u03c5\u03c4\u03ac \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03af\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ac.", + "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS" + }, + "hassio_confirm": { + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Z-Wave JS \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS" + }, + "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 Z-Wave JS \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" + }, + "manual": { + "data": { + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS Supervisor" + }, + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS Supervisor;", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, "start_addon": { "title": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS \u03be\u03b5\u03ba\u03b9\u03bd\u03ac." }, @@ -19,9 +63,85 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "\u039a\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf {entity_name}", + "ping": "Ping \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "refresh_value": "\u0391\u03bd\u03b1\u03bd\u03b5\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b3\u03b9\u03b1 {entity_name}", + "reset_meter": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ce\u03bd \u03c3\u03c4\u03bf {subtype}", + "set_config_parameter": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 {subtype}", + "set_lock_usercode": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03c4\u03bf {entity_name}", + "set_value": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae \u03bc\u03b9\u03b1\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 Z-Wave" + }, + "condition_type": { + "config_parameter": "\u03a4\u03b9\u03bc\u03ae \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5 {subtype}", + "node_status": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5", + "value": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c4\u03b9\u03bc\u03ae \u03bc\u03b9\u03b1\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 Z-Wave" + }, "trigger_type": { + "event.notification.entry_control": "\u0391\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "event.notification.notification": "\u0391\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", + "event.value_notification.basic": "\u0392\u03b1\u03c3\u03b9\u03ba\u03cc \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd CC \u03c3\u03c4\u03bf {subtype}", + "event.value_notification.central_scene": "\u0394\u03c1\u03ac\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ae\u03c2 \u03c3\u03ba\u03b7\u03bd\u03ae\u03c2 \u03c3\u03c4\u03bf {subtype}", + "event.value_notification.scene_activation": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03ba\u03b7\u03bd\u03ae\u03c2 \u03c3\u03c4\u03bf {subtype}", + "state.node_status": "\u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 \u03ac\u03bb\u03bb\u03b1\u03be\u03b5", "zwave_js.value_updated.config_parameter": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b9\u03bc\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf config {subtype}", "zwave_js.value_updated.value": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b9\u03bc\u03ae\u03c2 \u03c3\u03b5 \u03c4\u03b9\u03bc\u03ae Z-Wave JS" } - } + }, + "options": { + "abort": { + "addon_get_discovery_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 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", + "addon_info_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03c9\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd Z-Wave JS.", + "addon_install_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", + "addon_set_config_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bf \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd JS Z-Wave.", + "addon_start_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS.", + "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", + "different_device": "\u0397 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae USB \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03af\u03b4\u03b9\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_ws_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL websocket", + "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 Z-Wave JS. \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 Z-Wave JS. \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": { + "configure_addon": { + "data": { + "emulate_hardware": "\u0395\u03be\u03bf\u03bc\u03bf\u03af\u03c9\u03c3\u03b7 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd", + "log_level": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", + "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", + "s0_legacy_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af S0 (\u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5)", + "s2_access_control_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 S2", + "s2_authenticated_key": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", + "s2_unauthenticated_key": "\u039c\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", + "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + }, + "description": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03c0\u03b5\u03b4\u03af\u03b1 \u03b1\u03c5\u03c4\u03ac \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03af\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ac.", + "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Z-Wave JS" + }, + "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 Z-Wave JS \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" + }, + "manual": { + "data": { + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS Supervisor" + }, + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS Supervisor;", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "start_addon": { + "title": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS \u03be\u03b5\u03ba\u03b9\u03bd\u03ac." + } + } + }, + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index bab70cd4b948e..d455b7befe68b 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -97,7 +97,7 @@ "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.", "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", - "different_device": "Il dispositivo USB connesso non \u00e8 lo stesso configurato in precedenza per questa voce di configurazione. Si prega, invece, di creare una nuova voce di configurazione per il nuovo dispositivo." + "different_device": "Il dispositivo USB connesso non \u00e8 lo stesso configurato in precedenza per questa voce di configurazione. Crea invece una nuova voce di configurazione per il nuovo dispositivo." }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json index e29d809ebff3d..b8fadd6508924 100644 --- a/homeassistant/components/zwave_js/translations/pt-BR.json +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -1,7 +1,147 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "addon_get_discovery_info_failed": "Falhou em obter informa\u00e7\u00f5es de descoberta do add-on Z-Wave JS.", + "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on Z-Wave JS.", + "addon_install_failed": "Falha ao instalar o add-on Z-Wave JS.", + "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o do Z-Wave JS.", + "addon_start_failed": "Falha ao iniciar o add-on Z-Wave JS.", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao conectar", + "discovery_requires_supervisor": "A descoberta requer o supervisor.", + "not_zwave_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Z-Wave." + }, + "error": { + "addon_start_failed": "Falha ao iniciar o add-on Z-Wave JS. Verifique a configura\u00e7\u00e3o.", + "cannot_connect": "Falha ao conectar", + "invalid_ws_url": "URL de websocket inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "progress": { + "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do add-on Z-Wave JS termina. Isso pode levar v\u00e1rios minutos.", + "start_addon": "Aguarde enquanto a inicializa\u00e7\u00e3o do add-on Z-Wave JS \u00e9 conclu\u00edda. Isso pode levar alguns segundos." + }, + "step": { + "configure_addon": { + "data": { + "network_key": "Chave de rede", + "s0_legacy_key": "Chave S0 (Legado)", + "s2_access_control_key": "Chave de controle de acesso S2", + "s2_authenticated_key": "Chave autenticada S2", + "s2_unauthenticated_key": "Chave n\u00e3o autenticada S2", + "usb_path": "Caminho do Dispositivo USB" + }, + "description": "O add-on gerar\u00e1 chaves de seguran\u00e7a se esses campos forem deixados em vazios.", + "title": "Digite a configura\u00e7\u00e3o do add-on Z-Wave JS" + }, + "hassio_confirm": { + "title": "Configure a integra\u00e7\u00e3o Z-Wave JS com o add-on Z-Wave JS" + }, + "install_addon": { + "title": "A instala\u00e7\u00e3o do add-on Z-Wave JS foi iniciada" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use o add-on Z-Wave JS Supervisor" + }, + "description": "Deseja usar o add-on Z-Wave JS Supervisor?", + "title": "Selecione o m\u00e9todo de conex\u00e3o" + }, + "start_addon": { + "title": "O add-on Z-Wave JS est\u00e1 iniciando." + }, + "usb_confirm": { + "description": "Deseja configurar o {name} com o add-on Z-Wave JS?" + } } - } + }, + "device_automation": { + "action_type": { + "clear_lock_usercode": "Limpar o c\u00f3digo de usu\u00e1rio em {entity_name}", + "ping": "Ping dispositivo", + "refresh_value": "Atualize os valores para {entity_name}", + "reset_meter": "Redefinir medidores em {subtype}", + "set_config_parameter": "Definir valor do par\u00e2metro de configura\u00e7\u00e3o {subtype}", + "set_lock_usercode": "Defina um c\u00f3digo de usu\u00e1rio em {entity_name}", + "set_value": "Definir valor de um valor de onda Z" + }, + "condition_type": { + "config_parameter": "Valor do par\u00e2metro de configura\u00e7\u00e3o {subtype}", + "node_status": "Status do n\u00f3", + "value": "Valor atual de um Z-Wave" + }, + "trigger_type": { + "event.notification.entry_control": "Enviou uma notifica\u00e7\u00e3o de controle de entrada", + "event.notification.notification": "Enviou uma notifica\u00e7\u00e3o", + "event.value_notification.basic": "Evento CC b\u00e1sico em {subtype}", + "event.value_notification.central_scene": "A\u00e7\u00e3o da cena central em {subtype}", + "event.value_notification.scene_activation": "Ativa\u00e7\u00e3o de cena em {subtype}", + "state.node_status": "Status do n\u00f3 alterado", + "zwave_js.value_updated.config_parameter": "Altera\u00e7\u00e3o de valor no par\u00e2metro de configura\u00e7\u00e3o {subtype}", + "zwave_js.value_updated.value": "Altera\u00e7\u00e3o de valor em um valor Z-Wave JS" + } + }, + "options": { + "abort": { + "addon_get_discovery_info_failed": "Falha em obter informa\u00e7\u00f5es sobre a descoberta do add-on Z-Wave JS.", + "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on Z-Wave JS.", + "addon_install_failed": "Falha ao instalar o add-on Z-Wave JS.", + "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o do Z-Wave JS.", + "addon_start_failed": "Falha ao iniciar o complemento Z-Wave JS.", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", + "different_device": "O dispositivo USB conectado n\u00e3o \u00e9 o mesmo configurado anteriormente para esta entrada de configura\u00e7\u00e3o. Em vez disso, crie uma nova entrada de configura\u00e7\u00e3o para o novo dispositivo." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_ws_url": "URL de websocket inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "progress": { + "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do complemento Z-Wave JS termina. Isso pode levar v\u00e1rios minutos.", + "start_addon": "Aguarde enquanto a inicializa\u00e7\u00e3o do complemento Z-Wave JS \u00e9 conclu\u00edda. Isso pode levar alguns segundos." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emular hardware", + "log_level": "N\u00edvel de registro", + "network_key": "Chave de rede", + "s0_legacy_key": "Chave S0 (Legado)", + "s2_access_control_key": "Chave de controle de acesso S2", + "s2_authenticated_key": "Chave autenticada S2", + "s2_unauthenticated_key": "Chave n\u00e3o autenticada S2", + "usb_path": "Caminho do Dispositivo USB" + }, + "description": "O complemento gerar\u00e1 chaves de seguran\u00e7a se esses campos forem deixados em branco.", + "title": "Digite a configura\u00e7\u00e3o do add-on Z-Wave JS" + }, + "install_addon": { + "title": "A instala\u00e7\u00e3o do add-on Z-Wave JS foi iniciada" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use o add-on Z-Wave JS" + }, + "description": "Deseja usar o add-on Z-Wave JS?", + "title": "Selecione o m\u00e9todo de conex\u00e3o" + }, + "start_addon": { + "title": "O add-on Z-Wave JS est\u00e1 iniciando." + } + } + }, + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/sk.json b/homeassistant/components/zwave_js/translations/sk.json new file mode 100644 index 0000000000000..833d18faafbea --- /dev/null +++ b/homeassistant/components/zwave_js/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + } + }, + "options": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/trigger.py b/homeassistant/components/zwave_js/trigger.py index ca9bd7d24a297..07f89388e678b 100644 --- a/homeassistant/components/zwave_js/trigger.py +++ b/homeassistant/components/zwave_js/trigger.py @@ -12,10 +12,11 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from .triggers import value_updated +from .triggers import event, value_updated TRIGGERS = { "value_updated": value_updated, + "event": event, } diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py new file mode 100644 index 0000000000000..110cd21294f10 --- /dev/null +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -0,0 +1,232 @@ +"""Offer Z-Wave JS event listening automation trigger.""" +from __future__ import annotations + +import functools + +from pydantic import ValidationError +import voluptuous as vol +from zwave_js_server.client import Client +from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP +from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP +from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP, Node + +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.zwave_js.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_EVENT, + ATTR_EVENT_DATA, + ATTR_EVENT_SOURCE, + ATTR_NODE_ID, + ATTR_PARTIAL_DICT_MATCH, + DATA_CLIENT, + DOMAIN, +) +from homeassistant.components.zwave_js.helpers import ( + async_get_node_from_device_id, + async_get_node_from_entity_id, + get_device_id, + get_home_and_node_id_from_device_entry, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import ConfigType + +# Platform type should be . +PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" + +EVENT_MODEL_MAP = { + "controller": CONTROLLER_EVENT_MODEL_MAP, + "driver": DRIVER_EVENT_MODEL_MAP, + "node": NODE_EVENT_MODEL_MAP, +} + + +def validate_non_node_event_source(obj: dict) -> dict: + """Validate that a trigger for a non node event source has a config entry.""" + if obj[ATTR_EVENT_SOURCE] != "node" and ATTR_CONFIG_ENTRY_ID in obj: + return obj + raise vol.Invalid(f"Non node event triggers must contain {ATTR_CONFIG_ENTRY_ID}.") + + +def validate_event_name(obj: dict) -> dict: + """Validate that a trigger has a valid event name.""" + event_source = obj[ATTR_EVENT_SOURCE] + event_name = obj[ATTR_EVENT] + # the keys to the event source's model map are the event names + vol.In(EVENT_MODEL_MAP[event_source])(event_name) + return obj + + +def validate_event_data(obj: dict) -> dict: + """Validate that a trigger has a valid event data.""" + # Return if there's no event data to validate + if ATTR_EVENT_DATA not in obj: + return obj + + event_source = obj[ATTR_EVENT_SOURCE] + event_name = obj[ATTR_EVENT] + event_data = obj[ATTR_EVENT_DATA] + try: + EVENT_MODEL_MAP[event_source][event_name](**event_data) + except ValidationError as exc: + # Filter out required field errors if keys can be missing, and if there are + # still errors, raise an exception + if errors := [ + error for error in exc.errors() if error["type"] != "value_error.missing" + ]: + raise vol.MultipleInvalid(errors) from exc + return obj + + +TRIGGER_SCHEMA = vol.All( + cv.TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): PLATFORM_TYPE, + vol.Optional(ATTR_CONFIG_ENTRY_ID): str, + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_EVENT_SOURCE): vol.In(EVENT_MODEL_MAP), + vol.Required(ATTR_EVENT): cv.string, + vol.Optional(ATTR_EVENT_DATA): dict, + vol.Optional(ATTR_PARTIAL_DICT_MATCH, default=False): bool, + }, + ), + validate_event_name, + validate_event_data, + vol.Any( + validate_non_node_event_source, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), +) + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + if ATTR_CONFIG_ENTRY_ID not in config: + return config + + entry_id = config[ATTR_CONFIG_ENTRY_ID] + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + raise vol.Invalid(f"Config entry '{entry_id}' not found") + + if entry.state is not ConfigEntryState.LOADED: + raise vol.Invalid(f"Config entry '{entry_id}' not loaded") + + return config + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, + *, + platform_type: str = PLATFORM_TYPE, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + nodes: set[Node] = set() + if ATTR_DEVICE_ID in config: + nodes.update( + { + async_get_node_from_device_id(hass, device_id) + for device_id in config[ATTR_DEVICE_ID] + } + ) + if ATTR_ENTITY_ID in config: + nodes.update( + { + async_get_node_from_entity_id(hass, entity_id) + for entity_id in config[ATTR_ENTITY_ID] + } + ) + + event_source = config[ATTR_EVENT_SOURCE] + event_name = config[ATTR_EVENT] + event_data_filter = config.get(ATTR_EVENT_DATA, {}) + + unsubs = [] + job = HassJob(action) + + trigger_data = automation_info["trigger_data"] + + @callback + def async_on_event(event_data: dict, device: dr.DeviceEntry | None = None) -> None: + """Handle event.""" + for key, val in event_data_filter.items(): + if key not in event_data: + return + if ( + config[ATTR_PARTIAL_DICT_MATCH] + and isinstance(event_data[key], dict) + and isinstance(event_data_filter[key], dict) + ): + for key2, val2 in event_data_filter[key].items(): + if key2 not in event_data[key] or event_data[key][key2] != val2: + return + continue + if event_data[key] != val: + return + + payload = { + **trigger_data, + CONF_PLATFORM: platform_type, + ATTR_EVENT_SOURCE: event_source, + ATTR_EVENT: event_name, + ATTR_EVENT_DATA: event_data, + } + + primary_desc = f"Z-Wave JS '{event_source}' event '{event_name}' was emitted" + + if device: + device_name = device.name_by_user or device.name + payload[ATTR_DEVICE_ID] = device.id + home_and_node_id = get_home_and_node_id_from_device_entry(device) + assert home_and_node_id + payload[ATTR_NODE_ID] = home_and_node_id[1] + payload["description"] = f"{primary_desc} on {device_name}" + else: + payload["description"] = primary_desc + + payload[ + "description" + ] = f"{payload['description']} with event data: {event_data}" + + hass.async_run_hass_job(job, {"trigger": payload}) + + dev_reg = dr.async_get(hass) + + if not nodes: + entry_id = config[ATTR_CONFIG_ENTRY_ID] + client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + if event_source == "controller": + source = client.driver.controller + else: + source = client.driver + unsubs.append(source.on(event_name, async_on_event)) + + for node in nodes: + device_identifier = get_device_id(node.client, node) + device = dev_reg.async_get_device({device_identifier}) + assert device + # We need to store the device for the callback + unsubs.append( + node.on(event_name, functools.partial(async_on_event, device=device)) + ) + + @callback + def async_remove() -> None: + """Remove state listeners async.""" + for unsub in unsubs: + unsub() + unsubs.clear() + + return async_remove diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 7ebdb4f3748d4..71223c4ef1ed7 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -1,4 +1,4 @@ -"""Offer Z-Wave JS value updated listening automation rules.""" +"""Offer Z-Wave JS value updated listening automation trigger.""" from __future__ import annotations import functools diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py new file mode 100644 index 0000000000000..6e2ee0fd58c42 --- /dev/null +++ b/homeassistant/components/zwave_me/__init__.py @@ -0,0 +1,136 @@ +"""The Z-Wave-Me WS integration.""" +import asyncio +import logging + +from zwave_me_ws import ZWaveMe, ZWaveMeData + +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.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN, PLATFORMS, ZWaveMePlatform + +_LOGGER = logging.getLogger(__name__) +ZWAVE_ME_PLATFORMS = [platform.value for platform in ZWaveMePlatform] + + +async def async_setup_entry(hass, entry): + """Set up Z-Wave-Me from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + 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)) + return True + raise ConfigEntryNotReady() + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + controller = hass.data[DOMAIN].pop(entry.entry_id) + await controller.zwave_api.close_ws() + return unload_ok + + +class ZWaveMeController: + """Main ZWave-Me API class.""" + + def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None: + """Create the API instance.""" + self.device_ids: set = set() + self._hass = hass + self.config = config + self.zwave_api = ZWaveMe( + on_device_create=self.on_device_create, + on_device_update=self.on_device_update, + on_new_device=self.add_device, + token=self.config.data[CONF_TOKEN], + url=self.config.data[CONF_URL], + platforms=ZWAVE_ME_PLATFORMS, + ) + self.platforms_inited = False + + async def async_establish_connection(self): + """Get connection status.""" + is_connected = await self.zwave_api.get_connection() + return is_connected + + 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) + + def on_device_create(self, devices: list) -> None: + """Create multiple devices.""" + for device in devices: + 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) + + +async def async_setup_platforms( + hass: HomeAssistant, entry: ConfigEntry, controller: ZWaveMeController +) -> None: + """Set up platforms.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] + ) + controller.platforms_inited = True + + await hass.async_add_executor_job(controller.zwave_api.get_devices) + + +class ZWaveMeEntity(Entity): + """Representation of a ZWaveMe device.""" + + def __init__(self, controller, device): + """Initialize the device.""" + self.controller = controller + self.device = device + self._attr_name = device.title + self._attr_unique_id: str = ( + f"{self.controller.config.unique_id}-{self.device.id}" + ) + self._attr_should_poll = False + + @property + def device_info(self) -> DeviceInfo: + """Return device specific attributes.""" + return DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, + name=self._attr_name, + manufacturer=self.device.manufacturer, + sw_version=self.device.firmware, + suggested_area=self.device.locationName, + ) + + async def async_added_to_hass(self) -> None: + """Connect to an updater.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"ZWAVE_ME_INFO_{self.device.id}", self.get_new_data + ) + ) + + @callback + def get_new_data(self, new_data): + """Update info in the HAss.""" + self.device = new_data + self._attr_available = not new_data.isFailed + self.async_write_ha_state() diff --git a/homeassistant/components/zwave_me/binary_sensor.py b/homeassistant/components/zwave_me/binary_sensor.py new file mode 100644 index 0000000000000..40d850b8483d2 --- /dev/null +++ b/homeassistant/components/zwave_me/binary_sensor.py @@ -0,0 +1,75 @@ +"""Representation of a sensorBinary.""" +from __future__ import annotations + +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZWaveMeController, ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +BINARY_SENSORS_MAP: dict[str, BinarySensorEntityDescription] = { + "generic": BinarySensorEntityDescription( + key="generic", + ), + "motion": BinarySensorEntityDescription( + key="motion", + device_class=DEVICE_CLASS_MOTION, + ), +} +DEVICE_NAME = ZWaveMePlatform.BINARY_SENSOR + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + + @callback + def add_new_device(new_device: ZWaveMeData) -> None: + controller: ZWaveMeController = hass.data[DOMAIN][config_entry.entry_id] + description = BINARY_SENSORS_MAP.get( + new_device.probeType, BINARY_SENSORS_MAP["generic"] + ) + sensor = ZWaveMeBinarySensor(controller, new_device, description) + + async_add_entities( + [ + sensor, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeBinarySensor(ZWaveMeEntity, BinarySensorEntity): + """Representation of a ZWaveMe binary sensor.""" + + def __init__( + self, + controller: ZWaveMeController, + device: ZWaveMeData, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the device.""" + super().__init__(controller=controller, device=device) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self.device.level == "on" diff --git a/homeassistant/components/zwave_me/button.py b/homeassistant/components/zwave_me/button.py new file mode 100644 index 0000000000000..40105d100d411 --- /dev/null +++ b/homeassistant/components/zwave_me/button.py @@ -0,0 +1,40 @@ +"""Representation of a toggleButton.""" +from typing import Any + +from homeassistant.components.button import ButtonEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.BUTTON + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the number platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + button = ZWaveMeButton(controller, new_device) + + async_add_entities( + [ + button, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeButton(ZWaveMeEntity, ButtonEntity): + """Representation of a ZWaveMe button.""" + + def press(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.controller.zwave_api.send_command(self.device.id, "on") diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py new file mode 100644 index 0000000000000..140c397ecdec1 --- /dev/null +++ b/homeassistant/components/zwave_me/climate.py @@ -0,0 +1,101 @@ +"""Representation of a thermostat.""" +from __future__ import annotations + +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +TEMPERATURE_DEFAULT_STEP = 0.5 + +DEVICE_NAME = ZWaveMePlatform.CLIMATE + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the climate platform.""" + + @callback + def add_new_device(new_device: ZWaveMeData) -> None: + """Add a new device.""" + controller = hass.data[DOMAIN][config_entry.entry_id] + climate = ZWaveMeClimate(controller, new_device) + + async_add_entities( + [ + climate, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity): + """Representation of a ZWaveMe sensor.""" + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + + self.controller.zwave_api.send_command( + self.device.id, f"exact?level={temperature}" + ) + + @property + def temperature_unit(self) -> str: + """Return the temperature_unit.""" + return self.device.scaleTitle + + @property + def target_temperature(self) -> float: + """Return the state of the sensor.""" + return self.device.level + + @property + def max_temp(self) -> float: + """Return min temperature for the device.""" + return self.device.max + + @property + def min_temp(self) -> float: + """Return max temperature for the device.""" + return self.device.min + + @property + def hvac_modes(self) -> list[str]: + """Return the list of available operation modes.""" + return [HVAC_MODE_HEAT] + + @property + def hvac_mode(self) -> str: + """Return the current mode.""" + return HVAC_MODE_HEAT + + @property + def supported_features(self) -> int: + """Return the supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def target_temperature_step(self) -> float: + """Return the supported step of target temperature.""" + return TEMPERATURE_DEFAULT_STEP diff --git a/homeassistant/components/zwave_me/config_flow.py b/homeassistant/components/zwave_me/config_flow.py new file mode 100644 index 0000000000000..a4b257bdab5e0 --- /dev/null +++ b/homeassistant/components/zwave_me/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow to configure ZWaveMe integration.""" + +import logging + +from url_normalize import url_normalize +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_TOKEN, CONF_URL + +from . import helpers +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """ZWaveMe integration config flow.""" + + def __init__(self): + """Initialize flow.""" + self.url = None + self.token = None + self.uuid = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user or started with zeroconf.""" + errors = {} + if self.url is None: + schema = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_TOKEN): str, + } + ) + else: + schema = vol.Schema( + { + vol.Required(CONF_TOKEN): str, + } + ) + + if user_input is not None: + if self.url is None: + self.url = user_input[CONF_URL] + + self.token = user_input[CONF_TOKEN] + if not self.url.startswith(("ws://", "wss://")): + self.url = f"ws://{self.url}" + self.url = url_normalize(self.url, default_scheme="ws") + if self.uuid is None: + self.uuid = await helpers.get_uuid(self.url, self.token) + if self.uuid is not None: + await self.async_set_unique_id(self.uuid, raise_on_progress=False) + self._abort_if_unique_id_configured() + else: + errors["base"] = "no_valid_uuid_set" + + if not errors: + return self.async_create_entry( + title=self.url, + data={CONF_URL: self.url, CONF_TOKEN: self.token}, + ) + + return self.async_show_form( + step_id="user", + data_schema=schema, + errors=errors, + ) + + async def async_step_zeroconf(self, discovery_info): + """ + Handle a discovered Z-Wave accessory - get url to pass into user step. + + This flow is triggered by the discovery component. + """ + self.url = discovery_info.host + self.uuid = await helpers.get_uuid(self.url) + if self.uuid is None: + return self.async_abort(reason="no_valid_uuid_set") + + await self.async_set_unique_id(self.uuid) + self._abort_if_unique_id_configured() + return await self.async_step_user() diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py new file mode 100644 index 0000000000000..ccbf6989f070c --- /dev/null +++ b/homeassistant/components/zwave_me/const.py @@ -0,0 +1,32 @@ +"""Constants for ZWaveMe.""" +from homeassistant.backports.enum import StrEnum +from homeassistant.const import Platform + +# Base component constants +DOMAIN = "zwave_me" + + +class ZWaveMePlatform(StrEnum): + """Included ZWaveMe platforms.""" + + BINARY_SENSOR = "sensorBinary" + BUTTON = "toggleButton" + CLIMATE = "thermostat" + LOCK = "doorlock" + NUMBER = "switchMultilevel" + SWITCH = "switchBinary" + SENSOR = "sensorMultilevel" + RGBW_LIGHT = "switchRGBW" + RGB_LIGHT = "switchRGB" + + +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] diff --git a/homeassistant/components/zwave_me/helpers.py b/homeassistant/components/zwave_me/helpers.py new file mode 100644 index 0000000000000..0d53512d1cb97 --- /dev/null +++ b/homeassistant/components/zwave_me/helpers.py @@ -0,0 +1,14 @@ +"""Helpers for zwave_me config flow.""" +from __future__ import annotations + +from zwave_me_ws import ZWaveMe + + +async def get_uuid(url: str, token: str | None = None) -> str | None: + """Get an uuid from Z-Wave-Me.""" + conn = ZWaveMe(url=url, token=token) + uuid = None + if await conn.get_connection(): + uuid = await conn.get_uuid() + await conn.close_ws() + return uuid diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py new file mode 100644 index 0000000000000..df2a14d3bbde8 --- /dev/null +++ b/homeassistant/components/zwave_me/light.py @@ -0,0 +1,85 @@ +"""Representation of an RGB light.""" +from __future__ import annotations + +from typing import Any + +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.light import ATTR_RGB_COLOR, COLOR_MODE_RGB, LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the rgb platform.""" + + @callback + def add_new_device(new_device: ZWaveMeData) -> None: + """Add a new device.""" + controller = hass.data[DOMAIN][config_entry.entry_id] + rgb = ZWaveMeRGB(controller, new_device) + + async_add_entities( + [ + rgb, + ] + ) + + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.RGB_LIGHT.upper()}", add_new_device + ) + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.RGBW_LIGHT.upper()}", add_new_device + ) + + +class ZWaveMeRGB(ZWaveMeEntity, LightEntity): + """Representation of a ZWaveMe light.""" + + def turn_off(self, **kwargs: Any) -> None: + """Turn the device on.""" + self.controller.zwave_api.send_command(self.device.id, "off") + + def turn_on(self, **kwargs: Any): + """Turn the device on.""" + color = kwargs.get(ATTR_RGB_COLOR) + + if color is None: + color = (122, 122, 122) + cmd = "exact?red={}&green={}&blue={}".format(*color) + self.controller.zwave_api.send_command(self.device.id, cmd) + + @property + def is_on(self) -> bool: + """Return true if the light is on.""" + return self.device.level == "on" + + @property + def brightness(self) -> int: + """Return the brightness of a device.""" + return max(self.device.color.values()) + + @property + def rgb_color(self) -> tuple[int, int, int]: + """Return the rgb color value [int, int, int].""" + rgb = self.device.color + return rgb["r"], rgb["g"], rgb["b"] + + @property + def supported_color_modes(self) -> set: + """Return all color modes.""" + return {COLOR_MODE_RGB} + + @property + def color_mode(self) -> str: + """Return current color mode.""" + return COLOR_MODE_RGB diff --git a/homeassistant/components/zwave_me/lock.py b/homeassistant/components/zwave_me/lock.py new file mode 100644 index 0000000000000..17e64ff1602ea --- /dev/null +++ b/homeassistant/components/zwave_me/lock.py @@ -0,0 +1,60 @@ +"""Representation of a doorlock.""" +from __future__ import annotations + +from typing import Any + +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.LOCK + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the lock platform.""" + + @callback + def add_new_device(new_device: ZWaveMeData) -> None: + """Add a new device.""" + controller = hass.data[DOMAIN][config_entry.entry_id] + lock = ZWaveMeLock(controller, new_device) + + async_add_entities( + [ + lock, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeLock(ZWaveMeEntity, LockEntity): + """Representation of a ZWaveMe lock.""" + + @property + def is_locked(self) -> bool: + """Return the state of the lock.""" + return self.device.level == "close" + + def unlock(self, **kwargs: Any) -> None: + """Send command to unlock the lock.""" + self.controller.zwave_api.send_command(self.device.id, "open") + + def lock(self, **kwargs: Any) -> None: + """Send command to lock the lock.""" + self.controller.zwave_api.send_command(self.device.id, "close") diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json new file mode 100644 index 0000000000000..8863cd6ebf7ec --- /dev/null +++ b/homeassistant/components/zwave_me/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "zwave_me", + "name": "Z-Wave.Me", + "documentation": "https://www.home-assistant.io/integrations/zwave_me", + "iot_class": "local_push", + "requirements": [ + "zwave_me_ws==0.2.1", + "url-normalize==1.4.1" + ], + "after_dependencies": ["zeroconf"], + "zeroconf": [{"type":"_hap._tcp.local.", "name": "*z.wave-me*"}], + "config_flow": true, + "codeowners": [ + "@lawfulchaos", + "@Z-Wave-Me" + ] +} diff --git a/homeassistant/components/zwave_me/number.py b/homeassistant/components/zwave_me/number.py new file mode 100644 index 0000000000000..b955ade21db17 --- /dev/null +++ b/homeassistant/components/zwave_me/number.py @@ -0,0 +1,45 @@ +"""Representation of a switchMultilevel.""" +from homeassistant.components.number import NumberEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.NUMBER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the number platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + switch = ZWaveMeNumber(controller, new_device) + + async_add_entities( + [ + switch, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeNumber(ZWaveMeEntity, NumberEntity): + """Representation of a ZWaveMe Multilevel Switch.""" + + @property + def value(self): + """Return the unit of measurement.""" + return self.device.level + + def set_value(self, value: float) -> None: + """Update the current value.""" + self.controller.zwave_api.send_command( + self.device.id, f"exact?level={str(round(value))}" + ) diff --git a/homeassistant/components/zwave_me/sensor.py b/homeassistant/components/zwave_me/sensor.py new file mode 100644 index 0000000000000..84c4f40649597 --- /dev/null +++ b/homeassistant/components/zwave_me/sensor.py @@ -0,0 +1,120 @@ +"""Representation of a sensorMultilevel.""" +from __future__ import annotations + +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + LIGHT_LUX, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ZWaveMeController, ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +SENSORS_MAP: dict[str, SensorEntityDescription] = { + "meterElectric_watt": SensorEntityDescription( + key="meterElectric_watt", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + "meterElectric_kilowatt_hour": SensorEntityDescription( + key="meterElectric_kilowatt_hour", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "meterElectric_voltage": SensorEntityDescription( + key="meterElectric_voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + "light": SensorEntityDescription( + key="light", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + "noise": SensorEntityDescription( + key="noise", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + ), + "currentTemperature": SensorEntityDescription( + key="currentTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "temperature": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "generic": SensorEntityDescription( + key="generic", + ), +} +DEVICE_NAME = ZWaveMePlatform.SENSOR + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + + @callback + def add_new_device(new_device: ZWaveMeData) -> None: + controller: ZWaveMeController = hass.data[DOMAIN][config_entry.entry_id] + description = SENSORS_MAP.get(new_device.probeType, SENSORS_MAP["generic"]) + sensor = ZWaveMeSensor(controller, new_device, description) + + async_add_entities( + [ + sensor, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeSensor(ZWaveMeEntity, SensorEntity): + """Representation of a ZWaveMe sensor.""" + + def __init__( + self, + controller: ZWaveMeController, + device: ZWaveMeData, + description: SensorEntityDescription, + ) -> None: + """Initialize the device.""" + super().__init__(controller=controller, device=device) + self.entity_description = description + + @property + def native_value(self) -> str: + """Return the state of the sensor.""" + return self.device.level diff --git a/homeassistant/components/zwave_me/strings.json b/homeassistant/components/zwave_me/strings.json new file mode 100644 index 0000000000000..4986de744c0f8 --- /dev/null +++ b/homeassistant/components/zwave_me/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this).", + "data": { + "url": "[%key:common::config_flow::data::url%]", + "token": "Token" + } + } + }, + "error": { + "no_valid_uuid_set": "No valid UUID set" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_valid_uuid_set": "No valid UUID set" + } + } +} diff --git a/homeassistant/components/zwave_me/switch.py b/homeassistant/components/zwave_me/switch.py new file mode 100644 index 0000000000000..c759809df1501 --- /dev/null +++ b/homeassistant/components/zwave_me/switch.py @@ -0,0 +1,67 @@ +"""Representation of a switchBinary.""" +import logging +from typing import Any + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +_LOGGER = logging.getLogger(__name__) +DEVICE_NAME = ZWaveMePlatform.SWITCH + +SWITCH_MAP: dict[str, SwitchEntityDescription] = { + "generic": SwitchEntityDescription( + key="generic", + device_class=SwitchDeviceClass.SWITCH, + ) +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the switch platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + switch = ZWaveMeSwitch(controller, new_device, SWITCH_MAP["generic"]) + + async_add_entities( + [ + switch, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeSwitch(ZWaveMeEntity, SwitchEntity): + """Representation of a ZWaveMe binary switch.""" + + def __init__(self, controller, device, description): + """Initialize the device.""" + super().__init__(controller, device) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.device.level == "on" + + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.controller.zwave_api.send_command(self.device.id, "on") + + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self.controller.zwave_api.send_command(self.device.id, "off") diff --git a/homeassistant/components/zwave_me/translations/bg.json b/homeassistant/components/zwave_me/translations/bg.json new file mode 100644 index 0000000000000..34c3f5b94996b --- /dev/null +++ b/homeassistant/components/zwave_me/translations/bg.json @@ -0,0 +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" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/ca.json b/homeassistant/components/zwave_me/translations/ca.json new file mode 100644 index 0000000000000..a382cef549479 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_valid_uuid_set": "No hi ha cap UUID v\u00e0lid definit" + }, + "error": { + "no_valid_uuid_set": "No hi ha cap UUID v\u00e0lid definit" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Introdueix l'adre\u00e7a IP del servidor Z-Way i el 'token' d'acc\u00e9s Z-Way. Si s'utilitza HTTPS en lloc d'HTTP, l'adre\u00e7a IP es pot prefixar amb wss://. Per obtenir el 'token', v\u00e9s a la interf\u00edcie d'usuari de Z-Way > Men\u00fa > Configuraci\u00f3 > Usuari > Token API. Es recomana crear un nou usuari de Home Assistant i concedir-li acc\u00e9s als dispositius que necessitis controlar des de Home Assistant. Tamb\u00e9 \u00e9s possible utilitzar l'acc\u00e9s remot a trav\u00e9s de find.z-wave.me per connectar amb un Z-Way remot. Introdueix wss://find.z-wave.me al camp d'IP i copia el 'token' d'\u00e0mbit global (inicia sessi\u00f3 a Z-Way mitjan\u00e7ant find.z-wave.me)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/cs.json b/homeassistant/components/zwave_me/translations/cs.json new file mode 100644 index 0000000000000..f069ef7b324ed --- /dev/null +++ b/homeassistant/components/zwave_me/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json new file mode 100644 index 0000000000000..5afd788a3fbae --- /dev/null +++ b/homeassistant/components/zwave_me/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits eingerichtet", + "no_valid_uuid_set": "Keine g\u00fcltige UUID gesetzt" + }, + "error": { + "no_valid_uuid_set": "Keine g\u00fcltige UUID gesetzt" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Gib die IP-Adresse des Z-Way-Servers und das Z-Way-Zugangs-Token ein. Der IP-Adresse kann wss:// vorangestellt werden, wenn HTTPS anstelle von HTTP verwendet werden soll. Um das Token zu erhalten, gehe auf Z-Way-Benutzeroberfl\u00e4che > Men\u00fc > Einstellungen > Benutzer > API-Token. Es wird empfohlen, einen neuen Benutzer f\u00fcr Home Assistant zu erstellen und den Ger\u00e4ten, die du \u00fcber den Home Assistant steuern m\u00f6chtest, Zugriff zu gew\u00e4hren. Es ist auch m\u00f6glich, den Fernzugriff \u00fcber find.z-wave.me zu nutzen, um ein entferntes Z-Way zu verbinden. Gib wss://find.z-wave.me in das IP-Feld ein und kopiere das Token mit globalem Geltungsbereich (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/el.json b/homeassistant/components/zwave_me/translations/el.json new file mode 100644 index 0000000000000..af8efe8ea86ee --- /dev/null +++ b/homeassistant/components/zwave_me/translations/el.json @@ -0,0 +1,20 @@ +{ + "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", + "no_valid_uuid_set": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf UUID" + }, + "error": { + "no_valid_uuid_set": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf UUID" + }, + "step": { + "user": { + "data": { + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Way \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Z-Way. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 wss:// \u03b5\u03ac\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af HTTPS \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 HTTP. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Z-Way > \u039c\u03b5\u03bd\u03bf\u03cd > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 > \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API. \u03a0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03bd\u03ad\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u0395\u03af\u03bd\u03b1\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 find.z-wave.me \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 Z-Way. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf wss://find.z-wave.me \u03c3\u03c4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf IP \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03bc\u03b5 Global scope (\u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 find.z-wave.me \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/en.json b/homeassistant/components/zwave_me/translations/en.json new file mode 100644 index 0000000000000..81d09d5c350a8 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_valid_uuid_set": "No valid UUID set" + }, + "error": { + "no_valid_uuid_set": "No valid UUID set" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/es.json b/homeassistant/components/zwave_me/translations/es.json new file mode 100644 index 0000000000000..eab23bbd0fffd --- /dev/null +++ b/homeassistant/components/zwave_me/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_valid_uuid_set": "No se ha establecido un UUID v\u00e1lido" + }, + "error": { + "no_valid_uuid_set": "No se ha establecido un UUID v\u00e1lido" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Direcci\u00f3n IP de entrada del servidor Z-Way y token de acceso Z-Way. La direcci\u00f3n IP se puede prefijar con wss:// si se debe usar HTTPS en lugar de HTTP. Para obtener el token, vaya a la interfaz de usuario de Z-Way > Configuraci\u00f3n de > de men\u00fa > token de API de > de usuario. Se sugiere crear un nuevo usuario para Home Assistant y conceder acceso a los dispositivos que necesita controlar desde Home Assistant. Tambi\u00e9n es posible utilizar el acceso remoto a trav\u00e9s de find.z-wave.me para conectar un Z-Way remoto. Ingrese wss://find.z-wave.me en el campo IP y copie el token con alcance global (inicie sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/et.json b/homeassistant/components/zwave_me/translations/et.json new file mode 100644 index 0000000000000..c07b4b2b5f815 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_valid_uuid_set": "Kehtivat UUID-d pole m\u00e4\u00e4ratud" + }, + "error": { + "no_valid_uuid_set": "Kehtivat UUID-d pole m\u00e4\u00e4ratud" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Sisesta Z-Way serveri IP-aadress ja Z-Way juurdep\u00e4\u00e4suluba. Kui HTTP asemel tuleks kasutada HTTPS-i, v\u00f5ib IP-aadressile lisada eesliite wss://. Tokeni hankimiseks mine Z-Way kasutajaliidesesse > Men\u00fc\u00fc > Seaded > Kasutaja > API tunnus. Soovitatav on luua Home Assistantile uus kasutaja ja anda juurdep\u00e4\u00e4s seadmetele mida pead Home Assistandi abil juhtima. Kaug-Z-Way \u00fchendamiseks on v\u00f5imalik kasutada ka kaugjuurdep\u00e4\u00e4su l\u00e4bi find.z-wave.me. Sisesta IP v\u00e4ljale wss://find.z-wave.me ja kopeeri globaalse ulatusega luba (selleks logi Z-Waysse sisse saidi find.z-wave.me kaudu)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/fr.json b/homeassistant/components/zwave_me/translations/fr.json new file mode 100644 index 0000000000000..1705796cf77b9 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_valid_uuid_set": "Aucun ensemble UUID valide" + }, + "error": { + "no_valid_uuid_set": "Aucun ensemble UUID valide" + }, + "step": { + "user": { + "data": { + "token": "Jeton", + "url": "URL" + }, + "description": "Entrez l'adresse IP du serveur Z-Way et le jeton d'acc\u00e8s Z-Way. L'adresse IP peut \u00eatre pr\u00e9c\u00e9d\u00e9e de wss:// si HTTPS doit \u00eatre utilis\u00e9 \u00e0 la place de HTTP. Pour obtenir le jeton, acc\u00e9dez \u00e0 l'interface utilisateur Z-Way > Menu > Param\u00e8tres > Utilisateur > Jeton API. Il est sugg\u00e9r\u00e9 de cr\u00e9er un nouvel utilisateur pour Home Assistant et d'accorder l'acc\u00e8s aux appareils que vous devez contr\u00f4ler \u00e0 partir de Home Assistant. Il est \u00e9galement possible d'utiliser l'acc\u00e8s \u00e0 distance via find.z-wave.me pour connecter un Z-Way distant. Entrez wss://find.z-wave.me dans le champ IP et copiez le jeton avec la port\u00e9e globale (connectez-vous \u00e0 Z-Way via find.z-wave.me pour cela)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/he.json b/homeassistant/components/zwave_me/translations/he.json new file mode 100644 index 0000000000000..5689db627e2a3 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/hu.json b/homeassistant/components/zwave_me/translations/hu.json new file mode 100644 index 0000000000000..679604d6cfa7d --- /dev/null +++ b/homeassistant/components/zwave_me/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_valid_uuid_set": "Nincs \u00e9rv\u00e9nyes UUID be\u00e1ll\u00edtva" + }, + "error": { + "no_valid_uuid_set": "Nincs \u00e9rv\u00e9nyes UUID be\u00e1ll\u00edtva" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Adja meg a Z-Way szerver IP-c\u00edm\u00e9t \u00e9s a Z-Way hozz\u00e1f\u00e9r\u00e9si tokent. Az IP-c\u00edm el\u00e9 wss:// el\u0151tagot lehet tenni, ha HTTP helyett HTTPS-t kell haszn\u00e1lni. A token megszerz\u00e9s\u00e9hez l\u00e9pjen a Z-Way felhaszn\u00e1l\u00f3i fel\u00fcletre > Men\u00fc > Be\u00e1ll\u00edt\u00e1sok > Felhaszn\u00e1l\u00f3 > API token men\u00fcpontba. Javasoljuk, hogy hozzon l\u00e9tre egy \u00faj felhaszn\u00e1l\u00f3t a Home Assistanthoz, \u00e9s adjon hozz\u00e1f\u00e9r\u00e9st azoknak az eszk\u00f6z\u00f6knek, amelyeket a Home Assistantb\u00f3l kell vez\u00e9relnie. A t\u00e1voli Z-Way csatlakoztat\u00e1s\u00e1hoz a find.z-wave.me oldalon kereszt\u00fcl t\u00e1voli hozz\u00e1f\u00e9r\u00e9st is haszn\u00e1lhat. \u00cdrja be az IP mez\u0151be a wss://find.z-wave.me c\u00edmet, \u00e9s m\u00e1solja be a tokent glob\u00e1lis hat\u00f3k\u00f6rrel (ehhez jelentkezzen be a Z-Way-be a find.z-wave.me oldalon kereszt\u00fcl)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/id.json b/homeassistant/components/zwave_me/translations/id.json new file mode 100644 index 0000000000000..da9ddeb6d6748 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_valid_uuid_set": "Tidak ada UUID yang valid ditetapkan" + }, + "error": { + "no_valid_uuid_set": "Tidak ada UUID yang valid ditetapkan" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Masukkan alamat IP server Z-Way dan token akses Z-Way. Alamat IP dapat diawali dengan wss:// jika HTTPS harus digunakan sebagai pengganti HTTP. Untuk mendapatkan token, buka antarmuka pengguna Z-Way > Menu > Pengaturan > Token API > Pengguna. Disarankan untuk membuat pengguna baru untuk Home Assistant dan memberikan akses ke perangkat yang perlu Anda kontrol dari Home Assistant. Dimungkinkan juga untuk menggunakan akses jarak jauh melalui find.z-wave.me untuk menghubungkan Z-Way jarak jauh. Masukkan wss://find.z-wave.me di bidang IP dan salin token dengan cakupan Global (masuk ke Z-Way melalui find.z-wave.me untuk ini)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/it.json b/homeassistant/components/zwave_me/translations/it.json new file mode 100644 index 0000000000000..d60ac60d899ff --- /dev/null +++ b/homeassistant/components/zwave_me/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_valid_uuid_set": "Nessun UUID valido impostato" + }, + "error": { + "no_valid_uuid_set": "Nessun UUID valido impostato" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Digita l'indirizzo IP del server Z-Way e il token di accesso Z-Way. L'indirizzo IP pu\u00f2 essere preceduto da wss:// se si deve utilizzare HTTPS invece di HTTP. Per ottenere il token, vai all'interfaccia utente di Z-Way > Menu > Impostazioni > Utente > Token API. Si consiglia di creare un nuovo utente per Home Assistant e concedere l'accesso ai dispositivi che devi controllare da Home Assistant. \u00c8 anche possibile utilizzare l'accesso remoto tramite find.z-wave.me per connettere uno Z-Way remoto. Inserisci wss://find.z-wave.me nel campo IP e copia il token con ambito globale (accedi a Z-Way tramite find.z-wave.me per questo)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/ja.json b/homeassistant/components/zwave_me/translations/ja.json new file mode 100644 index 0000000000000..2816ea011e4fa --- /dev/null +++ b/homeassistant/components/zwave_me/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", + "no_valid_uuid_set": "\u6709\u52b9\u306aUUID\u30bb\u30c3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093" + }, + "error": { + "no_valid_uuid_set": "\u6709\u52b9\u306aUUID\u30bb\u30c3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093" + }, + "step": { + "user": { + "data": { + "token": "\u30c8\u30fc\u30af\u30f3", + "url": "URL" + }, + "description": "Z-Way\u30b5\u30fc\u30d0\u30fc\u306eIP\u30a2\u30c9\u30ec\u30b9\u3068Z-Way\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u307e\u3059\u3002HTTP\u306e\u4ee3\u308f\u308a\u306bHTTPS\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u306e\u524d\u306b\u3001wss://\u3092\u4ed8\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3059\u308b\u306b\u306f\u3001Z-Way user interface > Menu > Settings > User > API token \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002Home Assistant\u306e\u65b0\u3057\u3044\u30e6\u30fc\u30b6\u30fc\u3092\u4f5c\u6210\u3057\u3001Home Assistant\u304b\u3089\u5236\u5fa1\u3059\u308b\u5fc5\u8981\u306e\u3042\u308b\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b\u3053\u3068\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002find.z-wave.me\u3092\u4ecb\u3057\u305f\u30ea\u30e2\u30fc\u30c8\u30a2\u30af\u30bb\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3001\u30ea\u30e2\u30fc\u30c8Z-Way\u3092\u63a5\u7d9a\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002IP\u30d5\u30a3\u30fc\u30eb\u30c9\u306b\u3001wss://find.z-wave.me \u3092\u5165\u529b\u3057\u3001\u30b0\u30ed\u30fc\u30d0\u30eb\u30b9\u30b3\u30fc\u30d7\u3067\u30c8\u30fc\u30af\u30f3\u3092\u30b3\u30d4\u30fc\u3057\u307e\u3059\uff08\u3053\u308c\u306b\u3064\u3044\u3066\u306f\u3001find.z-wave.me\u3092\u4ecb\u3057\u3066Z-Way\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\uff09\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/nl.json b/homeassistant/components/zwave_me/translations/nl.json new file mode 100644 index 0000000000000..a2356ed1035e0 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "no_valid_uuid_set": "Geen geldige UUID ingesteld" + }, + "error": { + "no_valid_uuid_set": "Geen geldige UUID ingesteld" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Voer het IP-adres van de Z-Way server en het Z-Way toegangstoken in. Het IP adres kan voorafgegaan worden door wss:// indien HTTPS gebruikt moet worden in plaats van HTTP. Om het token te verkrijgen gaat u naar de Z-Way gebruikersinterface > Menu > Instellingen > Gebruiker > API token. Het is aanbevolen om een nieuwe gebruiker voor Home Assistant aan te maken en toegang te verlenen aan apparaten die u wilt bedienen vanuit Home Assistant. Het is ook mogelijk om toegang op afstand te gebruiken via find.z-wave.me om een Z-Way op afstand te verbinden. Voer wss://find.z-wave.me in het IP veld in en kopieer het token met Global scope (log hiervoor in op Z-Way via find.z-wave.me)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/no.json b/homeassistant/components/zwave_me/translations/no.json new file mode 100644 index 0000000000000..8a9a6928e9345 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_valid_uuid_set": "Ingen gyldig UUID satt" + }, + "error": { + "no_valid_uuid_set": "Ingen gyldig UUID satt" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Skriv inn IP-adressen til Z-Way-serveren og Z-Way-tilgangstoken. IP-adressen kan settes foran med wss:// hvis HTTPS skal brukes i stedet for HTTP. For \u00e5 f\u00e5 tokenet, g\u00e5 til Z-Way-brukergrensesnittet > Meny > Innstillinger > Bruker > API-token. Det foresl\u00e5s \u00e5 opprette en ny bruker for Home Assistant og gi tilgang til enheter du m\u00e5 kontrollere fra Home Assistant. Det er ogs\u00e5 mulig \u00e5 bruke ekstern tilgang via find.z-wave.me for \u00e5 koble til en ekstern Z-Way. Skriv inn wss://find.z-wave.me i IP-feltet og kopier token med Global scope (logg inn p\u00e5 Z-Way via find.z-wave.me for dette)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/pl.json b/homeassistant/components/zwave_me/translations/pl.json new file mode 100644 index 0000000000000..d75251367cd52 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_valid_uuid_set": "Nie ustawiono prawid\u0142owego UUID" + }, + "error": { + "no_valid_uuid_set": "Nie ustawiono prawid\u0142owego UUID" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Wprowad\u017a adres IP serwera Z-Way i token dost\u0119pu Z-Way. Adres IP mo\u017ce by\u0107 poprzedzony wss://, je\u015bli zamiast HTTP powinien by\u0107 u\u017cywany HTTPS. Aby uzyska\u0107 token, przejd\u017a do interfejsu u\u017cytkownika Z-Way > Menu > Ustawienia > U\u017cytkownik > Token API. Sugeruje si\u0119 utworzenie nowego u\u017cytkownika Home Assistant i przyznanie dost\u0119pu do urz\u0105dze\u0144, kt\u00f3rymi chcesz sterowa\u0107 z Home Assistant. Mo\u017cliwe jest r\u00f3wnie\u017c u\u017cycie zdalnego dost\u0119pu za po\u015brednictwem find.z-wave.me, aby po\u0142\u0105czy\u0107 si\u0119 ze zdalnym Z-Way. Wpisz wss://find.z-wave.me w polu IP i skopiuj token z zakresem globalnym (w tym celu zaloguj si\u0119 do Z-Way przez find.z-wave.me)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/pt-BR.json b/homeassistant/components/zwave_me/translations/pt-BR.json new file mode 100644 index 0000000000000..4a7be43ddb709 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_valid_uuid_set": "Nenhum conjunto de UUID v\u00e1lido" + }, + "error": { + "no_valid_uuid_set": "Nenhum conjunto de UUID v\u00e1lido" + }, + "step": { + "user": { + "data": { + "token": "Token", + "url": "URL" + }, + "description": "Insira o endere\u00e7o IP do servidor Z-Way e o token de acesso Z-Way. O endere\u00e7o IP pode ser prefixado com wss:// e o HTTPS deve ser usado em vez de HTTP. Para obter o token, v\u00e1 para a interface do usu\u00e1rio Z-Way > Menu > Configura\u00e7\u00f5es > Usu\u00e1rio > Token de API. Sugere-se criar um novo usu\u00e1rio para o Home Assistant e conceder acesso aos dispositivos que voc\u00ea quer controlar no Home Assistant. Tamb\u00e9m \u00e9 poss\u00edvel usar o acesso remoto via find.z-wave.me para conectar um Z-Way remoto. Insira wss://find.z-wave.me no campo IP e copie o token com escopo Global (fa\u00e7a login no Z-Way via find.z-wave.me para isso)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/ru.json b/homeassistant/components/zwave_me/translations/ru.json new file mode 100644 index 0000000000000..d24b2f7327a9d --- /dev/null +++ b/homeassistant/components/zwave_me/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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.", + "no_valid_uuid_set": "\u041d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 UUID." + }, + "error": { + "no_valid_uuid_set": "\u041d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 UUID." + }, + "step": { + "user": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Z-Way \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Z-Way. \u0415\u0441\u043b\u0438 \u0432\u043c\u0435\u0441\u0442\u043e HTTP \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c HTTPS, IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c 'wss://'. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \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 Z-Way > \u041c\u0435\u043d\u044e > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c > \u0422\u043e\u043a\u0435\u043d API. \u0417\u0430\u0442\u0435\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f Home Assistant \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0431\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u0437 Home Assistant. \u0422\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0447\u0435\u0440\u0435\u0437 find.z-wave.me \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e Z-Way. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 wss://find.z-wave.me \u0432 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0441 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (\u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Z-Way \u0447\u0435\u0440\u0435\u0437 find.z-wave.me)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/tr.json b/homeassistant/components/zwave_me/translations/tr.json new file mode 100644 index 0000000000000..39d25423fab99 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_valid_uuid_set": "Ge\u00e7erli UUID seti yok" + }, + "error": { + "no_valid_uuid_set": "Ge\u00e7erli UUID seti yok" + }, + "step": { + "user": { + "data": { + "token": "Anahtar", + "url": "URL" + }, + "description": "Z-Way sunucusunun ve Z-Way eri\u015fim belirtecinin IP adresini girin. HTTP yerine HTTPS kullan\u0131lmas\u0131 gerekiyorsa, IP adresinin \u00f6n\u00fcne wss:// eklenebilir. Belirteci almak i\u00e7in Z-Way kullan\u0131c\u0131 aray\u00fcz\u00fc > Men\u00fc > Ayarlar > Kullan\u0131c\u0131 > API belirtecine gidin. Home Assistant i\u00e7in yeni bir kullan\u0131c\u0131 olu\u015fturman\u0131z ve Home Assistant'tan kontrol etmeniz gereken cihazlara eri\u015fim izni vermeniz \u00f6nerilir. Uzak bir Z-Way'i ba\u011flamak i\u00e7in find.z-wave.me arac\u0131l\u0131\u011f\u0131yla uzaktan eri\u015fimi kullanmak da m\u00fcmk\u00fcnd\u00fcr. IP alan\u0131na wss://find.z-wave.me yaz\u0131n ve belirteci Global kapsamla kopyalay\u0131n (bunun i\u00e7in find.z-wave.me arac\u0131l\u0131\u011f\u0131yla Z-Way'de oturum a\u00e7\u0131n)." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/zh-Hant.json b/homeassistant/components/zwave_me/translations/zh-Hant.json new file mode 100644 index 0000000000000..ee6eb1e9ae769 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_valid_uuid_set": "\u672a\u8a2d\u5b9a\u6709\u6548 UUID" + }, + "error": { + "no_valid_uuid_set": "\u672a\u8a2d\u5b9a\u6709\u6548 UUID" + }, + "step": { + "user": { + "data": { + "token": "\u6b0a\u6756", + "url": "\u7db2\u5740" + }, + "description": "\u8f38\u5165 Z-Way \u4f3a\u670d\u5668 IP \u4f4d\u5740\u8207 Z-Way \u5b58\u53d6\u6b0a\u6756\u3002\u5047\u5982\u4f7f\u7528 HTTPS \u800c\u975e HTTP\u3001IP \u4f4d\u5740\u524d\u7db4\u53ef\u80fd\u70ba wss://\u3002\u6b32\u53d6\u5f97\u6b0a\u6756\u3001\u8acb\u81f3 Z-Way \u4f7f\u7528\u8005\u4ecb\u9762 > \u9078\u55ae > \u8a2d\u5b9a > \u4f7f\u7528\u8005 > API \u6b0a\u6756\u7372\u5f97\u3002\u5efa\u8b70\u91dd\u5c0d Home Assistant \u65b0\u5275\u4f7f\u7528\u8005\u4e26\u7531 Home Assistant \u63a7\u5236\u53ef\u4ee5\u5b58\u53d6\u7684\u88dd\u7f6e\u3002\u53e6\u5916\u4e5f\u53ef\u4ee5\u900f\u904e find.z-wave.me \u9023\u7dda\u81f3\u9060\u7aef Z-Way \u4e26\u7372\u5f97\u9060\u7aef\u5b58\u53d6\u529f\u80fd\u3002\u65bc IP \u6b04\u4f4d\u8f38\u5165 wss://find.z-wave.me \u4e26\u7531 Global scope\uff08\u900f\u904e find.z-wave.me \u767b\u5165 Z-Way\uff09\u8907\u88fd\u6b0a\u6756\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/config.py b/homeassistant/config.py index 74a8055e97188..17a1c0fbfa1e0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -816,7 +816,7 @@ async def async_process_component_config( # noqa: C901 config_validator, "async_validate_config" ): try: - return await config_validator.async_validate_config( # type: ignore + return await config_validator.async_validate_config( # type: ignore[no-any-return] hass, config ) except (vol.Invalid, HomeAssistantError) as ex: @@ -829,7 +829,7 @@ async def async_process_component_config( # noqa: C901 # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: - return component.CONFIG_SCHEMA(config) # type: ignore + return component.CONFIG_SCHEMA(config) # type: ignore[no-any-return] except vol.Invalid as ex: async_log_exception(ex, domain, config, hass, integration.documentation) return None diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 32014be77748c..7bc37bcd3057a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Iterable, Mapping +from collections import ChainMap +from collections.abc import Awaitable, Callable, Iterable, Mapping from contextvars import ContextVar import dataclasses from enum import Enum @@ -36,6 +37,7 @@ _LOGGER = logging.getLogger(__name__) +SOURCE_DHCP = "dhcp" SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" SOURCE_HOMEKIT = "homekit" @@ -46,7 +48,6 @@ SOURCE_USB = "usb" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" -SOURCE_DHCP = "dhcp" # If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow # websocket command creates a config entry with this source and while it exists normal discoveries @@ -61,7 +62,7 @@ # This is used to signal that re-authentication is required by the user. SOURCE_REAUTH = "reauth" -HANDLERS = Registry() +HANDLERS: Registry[str, type[ConfigFlow]] = Registry() STORAGE_KEY = "core.config_entries" STORAGE_VERSION = 1 @@ -108,16 +109,16 @@ def recoverable(self) -> bool: DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" DISCOVERY_SOURCES = ( - SOURCE_SSDP, - SOURCE_USB, - SOURCE_DHCP, - SOURCE_HOMEKIT, - SOURCE_ZEROCONF, - SOURCE_HOMEKIT, SOURCE_DHCP, SOURCE_DISCOVERY, + SOURCE_HOMEKIT, SOURCE_IMPORT, + SOURCE_INTEGRATION_DISCOVERY, + SOURCE_MQTT, + SOURCE_SSDP, SOURCE_UNIGNORE, + SOURCE_USB, + SOURCE_ZEROCONF, ) RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" @@ -159,7 +160,7 @@ class OperationNotAllowed(ConfigError): """Raised when a config entry operation is not allowed.""" -UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Any] +UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Awaitable[None]] class ConfigEntry: @@ -174,6 +175,7 @@ class ConfigEntry: "options", "unique_id", "supports_unload", + "supports_remove_device", "pref_disable_new_entities", "pref_disable_polling", "source", @@ -256,6 +258,9 @@ def __init__( # Supports unload self.supports_unload = False + # Supports remove device + self.supports_remove_device = False + # Listeners to call on update self.update_listeners: list[ weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod @@ -286,6 +291,9 @@ async def async_setup( integration = await loader.async_get_integration(hass, self.domain) self.supports_unload = await support_entry_unload(hass, self.domain) + self.supports_remove_device = await support_remove_from_device( + hass, self.domain + ) try: component = integration.get_component() @@ -522,8 +530,10 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: ) return False # Handler may be a partial + # Keep for backwards compatibility + # https://github.com/home-assistant/core/pull/67087#discussion_r812559950 while isinstance(handler, functools.partial): - handler = handler.func + handler = handler.func # type: ignore[unreachable] if self.version == handler.VERSION: return True @@ -745,7 +755,7 @@ async def async_create_flow( if not context or "source" not in context: raise KeyError("Context not set or doesn't have a source set") - flow = cast(ConfigFlow, handler()) + flow = handler() flow.init_step = context["source"] return flow @@ -1211,7 +1221,10 @@ def _async_abort_entries_match( if match_dict is None: match_dict = {} # Match any entry for entry in self._async_current_entries(include_ignore=False): - if all(item in entry.data.items() for item in match_dict.items()): + if all( + item in ChainMap(entry.options, entry.data).items() # type: ignore[arg-type] + for item in match_dict.items() + ): raise data_entry_flow.AbortFlow("already_configured") @callback @@ -1393,12 +1406,24 @@ def async_abort( reason=reason, description_placeholders=description_placeholders ) + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by DHCP discovery.""" + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" return await self.async_step_discovery(discovery_info.config) + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by integration specific discovery.""" + return await self.async_step_discovery(discovery_info) + async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: @@ -1417,24 +1442,18 @@ async def async_step_ssdp( """Handle a flow initialized by SSDP discovery.""" return await self.async_step_discovery(dataclasses.asdict(discovery_info)) - async def async_step_zeroconf( - self, discovery_info: ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: - """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) - - async def async_step_dhcp( - self, discovery_info: DhcpServiceInfo - ) -> data_entry_flow.FlowResult: - """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) - async def async_step_usb( self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Zeroconf discovery.""" + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + @callback def async_create_entry( # pylint: disable=arguments-differ self, @@ -1479,7 +1498,7 @@ async def async_create_flow( if entry.domain not in HANDLERS: raise data_entry_flow.UnknownHandler - return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) + return HANDLERS[entry.domain].async_get_options_flow(entry) async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult @@ -1605,3 +1624,10 @@ async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: integration = await loader.async_get_integration(hass, domain) component = integration.get_component() return hasattr(component, "async_unload_entry") + + +async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool: + """Test if a domain supports being removed from a device.""" + integration = await loader.async_get_integration(hass, domain) + component = integration.get_component() + return hasattr(component, "async_remove_config_entry_device") diff --git a/homeassistant/const.py b/homeassistant/const.py index 1aa0b3cc35db8..c0ff111fc8993 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,13 +6,13 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 2 -PATCH_VERSION: Final = "9" +MINOR_VERSION: Final = 3 +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) -# Truthy date string triggers showing related deprecation warning messages. REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) +# Truthy date string triggers showing related deprecation warning messages. REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files diff --git a/homeassistant/core.py b/homeassistant/core.py index 30e98da763731..27dba3cbc5221 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -24,7 +24,6 @@ import re import threading from time import monotonic -from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, @@ -41,7 +40,7 @@ import voluptuous as vol import yarl -from . import async_timeout_backcompat, block_async_io, loader, util +from . import block_async_io, loader, util from .backports.enum import StrEnum from .const import ( ATTR_DOMAIN, @@ -83,13 +82,14 @@ run_callback_threadsafe, shutdown_run_callback_threadsafe, ) +from .util.read_only_dict import ReadOnlyDict from .util.timeout import TimeoutManager from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency if TYPE_CHECKING: from .auth import AuthManager - from .components.http import HomeAssistantHTTP + from .components.http import ApiConfig, HomeAssistantHTTP from .config_entries import ConfigEntries @@ -97,7 +97,6 @@ STAGE_2_SHUTDOWN_TIMEOUT = 60 STAGE_3_SHUTDOWN_TIMEOUT = 30 -async_timeout_backcompat.enable() block_async_io.enable() T = TypeVar("T") @@ -142,9 +141,12 @@ class ConfigSource(StrEnum): _LOGGER = logging.getLogger(__name__) -def split_entity_id(entity_id: str) -> list[str]: +def split_entity_id(entity_id: str) -> tuple[str, str]: """Split a state entity ID into domain and object ID.""" - return entity_id.split(".", 1) + domain, _, object_id = entity_id.partition(".") + if not domain or not object_id: + raise ValueError(f"Invalid entity ID {entity_id}") + return domain, object_id VALID_ENTITY_ID = re.compile(r"^(?!.+__)(?!_)[\da-z_]+(? None: """Initialize new Home Assistant object.""" @@ -350,7 +352,9 @@ async def async_start(self) -> None: self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) _async_create_timer(self) - def add_job(self, target: Callable[..., Any], *args: Any) -> None: + def add_job( + self, target: Callable[..., Any] | Coroutine[Any, Any, Any], *args: Any + ) -> None: """Add a job to be executed by the event loop or by an executor. If the job is either a coroutine or decorated with @callback, it will be @@ -764,7 +768,7 @@ def __repr__(self) -> str: def __eq__(self, other: Any) -> bool: """Return the comparison.""" - return ( # type: ignore + return ( # type: ignore[no-any-return] self.__class__ == other.__class__ and self.event_type == other.event_type and self.data == other.data @@ -1049,12 +1053,12 @@ def __init__( self.entity_id = entity_id.lower() self.state = state - self.attributes = MappingProxyType(attributes or {}) + self.attributes = ReadOnlyDict(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) - self._as_dict: dict[str, Collection[Any]] | None = None + self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None @property def name(self) -> str: @@ -1063,7 +1067,7 @@ def name(self) -> str: "_", " " ) - def as_dict(self) -> dict[str, Collection[Any]]: + def as_dict(self) -> ReadOnlyDict[str, Collection[Any]]: """Return a dict representation of the State. Async friendly. @@ -1077,14 +1081,16 @@ def as_dict(self) -> dict[str, Collection[Any]]: last_updated_isoformat = last_changed_isoformat else: last_updated_isoformat = self.last_updated.isoformat() - self._as_dict = { - "entity_id": self.entity_id, - "state": self.state, - "attributes": dict(self.attributes), - "last_changed": last_changed_isoformat, - "last_updated": last_updated_isoformat, - "context": self.context.as_dict(), - } + self._as_dict = ReadOnlyDict( + { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + "context": ReadOnlyDict(self.context.as_dict()), + } + ) return self._as_dict @classmethod @@ -1122,7 +1128,7 @@ def from_dict(cls: type[_StateT], json_dict: dict[str, Any]) -> _StateT | None: def __eq__(self, other: Any) -> bool: """Return the comparison of the state.""" - return ( # type: ignore + return ( # type: ignore[no-any-return] self.__class__ == other.__class__ and self.entity_id == other.entity_id and self.state == other.state @@ -1343,7 +1349,7 @@ def async_set( last_changed = None else: same_state = old_state.state == new_state and not force_update - same_attr = old_state.attributes == MappingProxyType(attributes) + same_attr = old_state.attributes == attributes last_changed = old_state.last_changed if same_state else None if same_state and same_attr: @@ -1404,7 +1410,7 @@ def __init__( """Initialize a service call.""" self.domain = domain.lower() self.service = service.lower() - self.data = MappingProxyType(data or {}) + self.data = ReadOnlyDict(data or {}) self.context = context or Context() def __repr__(self) -> str: @@ -1700,8 +1706,8 @@ def __init__(self, hass: HomeAssistant) -> None: # List of loaded components self.components: set[str] = set() - # API (HTTP) server configuration, see components.http.ApiConfig - self.api: Any | None = None + # API (HTTP) server configuration + self.api: ApiConfig | None = None # Directory that holds the configuration self.config_dir: str | None = None @@ -1918,7 +1924,7 @@ def schedule_tick(now: datetime.datetime) -> None: """Schedule a timer tick when the next second rolls around.""" nonlocal handle - slp_seconds = 1 - (now.microsecond / 10 ** 6) + slp_seconds = 1 - (now.microsecond / 10**6) target = monotonic() + slp_seconds handle = hass.loop.call_later(slp_seconds, fire_time_event, target) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 734a568ce4e1f..b69cf44dc6c31 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -378,11 +378,11 @@ class FlowHandler: # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - flow_id: str = None # type: ignore - hass: HomeAssistant = None # type: ignore - handler: str = None # type: ignore + flow_id: str = None # type: ignore[assignment] + hass: HomeAssistant = None # type: ignore[assignment] + handler: str = None # type: ignore[assignment] # Ensure the attribute has a subscriptable, but immutable, default value. - context: dict[str, Any] = MappingProxyType({}) # type: ignore + context: dict[str, Any] = MappingProxyType({}) # type: ignore[assignment] # Set by _async_create_flow callback init_step = "init" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bf64bc4a51c2d..5530c73ed5098 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -72,6 +72,7 @@ "dialogflow", "directv", "dlna_dmr", + "dlna_dms", "dnsip", "doorbird", "dsmr", @@ -95,6 +96,7 @@ "ezviz", "faa_delays", "fireservicerota", + "fivem", "fjaraskupan", "flick_electric", "flipr", @@ -159,6 +161,7 @@ "ipp", "iqvia", "islamic_prayer_times", + "iss", "isy994", "izone", "jellyfin", @@ -194,9 +197,11 @@ "mikrotik", "mill", "minecraft_server", + "mjpeg", "mobile_app", "modem_callerid", "modern_forms", + "moehlenhoff_alpha2", "monoprice", "motion_blinds", "motioneye", @@ -253,9 +258,11 @@ "progettihwsw", "prosegur", "ps4", + "pure_energie", "pvoutput", "pvpc_hourly_pricing", "rachio", + "radio_browser", "rainforest_eagle", "rainmachine", "rdw", @@ -283,6 +290,7 @@ "shopping_list", "sia", "simplisafe", + "sleepiq", "sma", "smappee", "smart_meter_texas", @@ -364,6 +372,7 @@ "wiffi", "wilight", "withings", + "wiz", "wled", "wolflink", "xbox", @@ -376,5 +385,6 @@ "zerproc", "zha", "zwave", - "zwave_js" + "zwave_js", + "zwave_me" ] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f05a7f73e50af..8633534e97660 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -2,620 +2,172 @@ To update, run python3 -m script.hassfest """ +from __future__ import annotations # fmt: off -DHCP = [ - { - "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": "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": "broadlink", - "macaddress": "34EA34*" - }, - { - "domain": "broadlink", - "macaddress": "24DFA7*" - }, - { - "domain": "broadlink", - "macaddress": "A043B0*" - }, - { - "domain": "broadlink", - "macaddress": "B4430D*" - }, - { - "domain": "emonitor", - "hostname": "emonitor*", - "macaddress": "0090C2*" - }, - { - "domain": "flume", - "hostname": "flume-gw-*" - }, - { - "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": "zengge_[0-9a-f][0-9a-f]_*" - }, - { - "domain": "flux_led", - "macaddress": "C82E47*", - "hostname": "sta*" - }, - { - "domain": "fronius", - "macaddress": "0003AC*" - }, - { - "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", - "hostname": "hunter*", - "macaddress": "002674*" - }, - { - "domain": "isy994", - "hostname": "isy*", - "macaddress": "0021B9*" - }, - { - "domain": "lyric", - "hostname": "lyric-*", - "macaddress": "48A2E6*" - }, - { - "domain": "lyric", - "hostname": "lyric-*", - "macaddress": "B82CA0*" - }, - { - "domain": "lyric", - "hostname": "lyric-*", - "macaddress": "00D02D*" - }, - { - "domain": "myq", - "macaddress": "645299*" - }, - { - "domain": "nest", - "macaddress": "18B430*" - }, - { - "domain": "nest", - "macaddress": "641666*" - }, - { - "domain": "nest", - "macaddress": "D8EB46*" - }, - { - "domain": "nest", - "macaddress": "1C53F9*" - }, - { - "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-*", - "macaddress": "88DA1A*" - }, - { - "domain": "powerwall", - "hostname": "1118431-*", - "macaddress": "000145*" - }, - { - "domain": "rachio", - "hostname": "rachio-*", - "macaddress": "009D6B*" - }, - { - "domain": "rachio", - "hostname": "rachio-*", - "macaddress": "F0038C*" - }, - { - "domain": "rachio", - "hostname": "rachio-*", - "macaddress": "74C63B*" - }, - { - "domain": "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": "samsungtv", - "hostname": "tizen*" - }, - { - "domain": "samsungtv", - "macaddress": "8CC8CD*" - }, - { - "domain": "samsungtv", - "macaddress": "606BBD*" - }, - { - "domain": "samsungtv", - "macaddress": "F47B5E*" - }, - { - "domain": "samsungtv", - "macaddress": "4844F7*" - }, - { - "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", - "macaddress": "20F85E*" - }, - { - "domain": "simplisafe", - "hostname": "simplisafe*", - "macaddress": "30AEA4*" - }, - { - "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", - "macaddress": "001E0C*", - "hostname": "my[45]50*" - }, - { - "domain": "tado", - "hostname": "tado*" - }, - { - "domain": "tesla_wall_connector", - "hostname": "teslawallconnector_*", - "macaddress": "DC44271*" - }, - { - "domain": "tesla_wall_connector", - "hostname": "teslawallconnector_*", - "macaddress": "98ED5C*" - }, - { - "domain": "tesla_wall_connector", - "hostname": "teslawallconnector_*", - "macaddress": "4CFCAA*" - }, - { - "domain": "tolo", - "hostname": "usr-tcp232-ed2" - }, - { - "domain": "toon", - "hostname": "eneco-*", - "macaddress": "74C63B*" - }, - { - "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": "403F8C*" - }, - { - "domain": "tplink", - "hostname": "k[lp]*", - "macaddress": "C0C9E3*" - }, - { - "domain": "tplink", - "hostname": "ep*", - "macaddress": "E848B8*" - }, - { - "domain": "tplink", - "hostname": "k[lp]*", - "macaddress": "E848B8*" - }, - { - "domain": "tplink", - "hostname": "k[lp]*", - "macaddress": "909A4A*" - }, - { - "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": "ep*", - "macaddress": "003192*" - }, - { - "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": "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": "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": "verisure", - "macaddress": "0023C1*" - }, - { - "domain": "vicare", - "macaddress": "B87424*" - }, - { - "domain": "yeelight", - "hostname": "yeelink-*" - } -] +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': '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': 'elkm1', 'registered_devices': True}, + {'domain': 'elkm1', 'macaddress': '00409D*'}, + {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, + {'domain': 'emonitor', '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': 'zengge_[0-9a-f][0-9a-f]_*'}, + {'domain': 'flux_led', 'hostname': 'sta*', 'macaddress': 'C82E47*'}, + {'domain': 'fronius', 'macaddress': '0003AC*'}, + {'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': 'isy994', 'registered_devices': True}, + {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, + {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, + {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, + {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, + {'domain': 'myq', 'macaddress': '645299*'}, + {'domain': 'nest', 'macaddress': '18B430*'}, + {'domain': 'nest', 'macaddress': '641666*'}, + {'domain': 'nest', 'macaddress': 'D8EB46*'}, + {'domain': 'nest', 'macaddress': '1C53F9*'}, + {'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': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'}, + {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'}, + {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '74C63B*'}, + {'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': 'samsungtv', 'registered_devices': True}, + {'domain': 'samsungtv', 'hostname': 'tizen*'}, + {'domain': 'samsungtv', 'macaddress': '8CC8CD*'}, + {'domain': 'samsungtv', 'macaddress': '606BBD*'}, + {'domain': 'samsungtv', 'macaddress': 'F47B5E*'}, + {'domain': 'samsungtv', 'macaddress': '4844F7*'}, + {'domain': 'samsungtv', 'macaddress': '8CEA48*'}, + {'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_*', + 'macaddress': 'DC44271*'}, + {'domain': 'tesla_wall_connector', + 'hostname': 'teslawallconnector_*', + 'macaddress': '98ED5C*'}, + {'domain': 'tesla_wall_connector', + 'hostname': 'teslawallconnector_*', + 'macaddress': '4CFCAA*'}, + {'domain': 'tolo', 'hostname': 'usr-tcp232-ed2'}, + {'domain': 'toon', 'hostname': 'eneco-*', 'macaddress': '74C63B*'}, + {'domain': 'tplink', 'registered_devices': True}, + {'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': '403F8C*'}, + {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': 'C0C9E3*'}, + {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': 'E848B8*'}, + {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': 'E848B8*'}, + {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '909A4A*'}, + {'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': 'ep*', 'macaddress': '003192*'}, + {'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': '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': '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/ssdp.py b/homeassistant/generated/ssdp.py index 1a243d954b9f0..f0117e2a9c2ad 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -97,6 +97,24 @@ "st": "urn:schemas-upnp-org:device:MediaRenderer:3" } ], + "dlna_dms": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", + "st": "urn:schemas-upnp-org:device:MediaServer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:2", + "st": "urn:schemas-upnp-org:device:MediaServer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:3", + "st": "urn:schemas-upnp-org:device:MediaServer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaServer:4", + "st": "urn:schemas-upnp-org:device:MediaServer:4" + } + ], "fritz": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index da48577a1460c..9c3776155e18f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -144,6 +144,10 @@ "_hap._tcp.local.": [ { "domain": "homekit_controller" + }, + { + "domain": "zwave_me", + "name": "*z.wave-me*" } ], "_homekit._tcp.local.": [ @@ -171,6 +175,10 @@ "manufacturer": "nettigo" } }, + { + "domain": "pure_energie", + "name": "smartbridge*" + }, { "domain": "rachio", "name": "rachio*" @@ -387,7 +395,12 @@ "Iota": "abode", "LIFX": "lifx", "MYQ": "myq", - "NL*": "nanoleaf", + "NL29": "nanoleaf", + "NL42": "nanoleaf", + "NL47": "nanoleaf", + "NL48": "nanoleaf", + "NL52": "nanoleaf", + "NL59": "nanoleaf", "Netatmo Relay": "netatmo", "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index f74aec0efe87e..281ab3108b633 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -42,5 +42,5 @@ def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: Async friendly. """ - pattern = re.compile(fr"^{domain}(| .+)$") + pattern = re.compile(rf"^{domain}(| .+)$") return [key for key in config.keys() if pattern.match(key)] diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 65b1b657ef451..eaabb002b0a4d 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -104,9 +104,9 @@ def _async_create_clientsession( # If a package requires a different user agent, override it by passing a headers # dictionary to the request method. # pylint: disable=protected-access - clientsession._default_headers = MappingProxyType({USER_AGENT: SERVER_SOFTWARE}) # type: ignore + clientsession._default_headers = MappingProxyType({USER_AGENT: SERVER_SOFTWARE}) # type: ignore[assignment] - clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore + clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore[assignment] if auto_cleanup_method: auto_cleanup_method(hass, clientsession) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index f6f9c968f104b..9017c60c23fab 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -330,7 +330,7 @@ def sync_entity_lifecycle( create_entity: Callable[[dict], Entity], ) -> None: """Map a collection to an entity component.""" - entities = {} + entities: dict[str, Entity] = {} ent_reg = entity_registry.async_get(hass) async def _add_entity(change_set: CollectionChangeSet) -> Entity: @@ -348,7 +348,7 @@ async def _remove_entity(change_set: CollectionChangeSet) -> None: entities.pop(change_set.item_id) async def _update_entity(change_set: CollectionChangeSet) -> None: - await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore + await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore[attr-defined] _func_map: dict[ str, Callable[[CollectionChangeSet], Coroutine[Any, Any, Entity | None]] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 80bed9137d0a0..3355424b71041 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -13,10 +13,7 @@ from typing import Any, cast from homeassistant.components import zone as zone_cmp -from homeassistant.components.device_automation import ( - DeviceAutomationType, - async_get_device_automation_platform, -) +from homeassistant.components.device_automation import condition as device_condition from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -30,7 +27,6 @@ CONF_BELOW, CONF_CONDITION, CONF_DEVICE_ID, - CONF_DOMAIN, CONF_ENTITY_ID, CONF_ID, CONF_STATE, @@ -872,15 +868,8 @@ async def async_device_from_config( hass: HomeAssistant, config: ConfigType ) -> ConditionCheckerType: """Test a device condition.""" - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION - ) - return trace_condition_function( - cast( - ConditionCheckerType, - platform.async_condition_from_config(hass, config), - ) - ) + checker = await device_condition.async_condition_from_config(hass, config) + return trace_condition_function(checker) async def async_trigger_from_config( @@ -936,21 +925,17 @@ async def async_validate_condition_config( sub_cond = await async_validate_condition_config(hass, sub_cond) conditions.append(sub_cond) config["conditions"] = conditions + return config if condition == "device": - config = cv.DEVICE_CONDITION_SCHEMA(config) - platform = await async_get_device_automation_platform( - hass, config[CONF_DOMAIN], DeviceAutomationType.CONDITION - ) - if hasattr(platform, "async_validate_condition_config"): - return await platform.async_validate_condition_config(hass, config) # type: ignore - return cast(ConfigType, platform.CONDITION_SCHEMA(config)) + return await device_condition.async_validate_condition_config(hass, config) if condition in ("numeric_state", "state"): - validator = getattr( - sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition) + validator = cast( + Callable[[HomeAssistant, ConfigType], ConfigType], + getattr(sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition)), ) - return validator(hass, config) # type: ignore + return validator(hass, config) return config diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index d7920f809410e..fddc5c8272591 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable import logging -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries from homeassistant.components import dhcp, mqtt, ssdp, zeroconf @@ -15,12 +15,13 @@ if TYPE_CHECKING: import asyncio -DiscoveryFunctionType = Callable[[HomeAssistant], Union[Awaitable[bool], bool]] +_R = TypeVar("_R", bound="Awaitable[bool] | bool") +DiscoveryFunctionType = Callable[[HomeAssistant], _R] _LOGGER = logging.getLogger(__name__) -class DiscoveryFlowHandler(config_entries.ConfigFlow): +class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): """Handle a discovery config flow.""" VERSION = 1 @@ -29,7 +30,7 @@ def __init__( self, domain: str, title: str, - discovery_function: DiscoveryFunctionType, + discovery_function: DiscoveryFunctionType[_R], ) -> None: """Initialize the discovery config flow.""" self._domain = domain @@ -153,7 +154,7 @@ async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: def register_discovery_flow( domain: str, title: str, - discovery_function: DiscoveryFunctionType, + discovery_function: DiscoveryFunctionType[Awaitable[bool] | bool], connection_class: str | UndefinedType = UNDEFINED, ) -> None: """Register flow for discovered integrations that not require auth.""" @@ -172,7 +173,7 @@ def register_discovery_flow( domain, ) - class DiscoveryFlow(DiscoveryFlowHandler): + class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): """Discovery flow handler.""" def __init__(self) -> None: diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 04e11ab99be55..cf2d73715dec3 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -217,7 +217,7 @@ def __init__(self) -> None: ) self.external_data: Any = None - self.flow_impl: AbstractOAuth2Implementation = None # type: ignore + self.flow_impl: AbstractOAuth2Implementation = None # type: ignore[assignment] @property @abstractmethod diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ed3c50cdb0020..7f33ec1f1ec9a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,7 +15,9 @@ from numbers import Number import os import re -from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed +from socket import ( # type: ignore[attr-defined] # private, not in typeshed + _GLOBAL_DEFAULT_TIMEOUT, +) from typing import Any, TypeVar, cast, overload from urllib.parse import urlparse from uuid import UUID @@ -163,7 +165,7 @@ def boolean(value: Any) -> bool: return False elif isinstance(value, Number): # type ignore: https://github.com/python/mypy/issues/3186 - return value != 0 # type: ignore + return value != 0 # type: ignore[comparison-overlap] raise vol.Invalid(f"invalid boolean value {value}") @@ -421,7 +423,7 @@ def date(value: Any) -> date_sys: def time_period_str(value: str) -> timedelta: """Validate and transform time offset.""" - if isinstance(value, int): # type: ignore + if isinstance(value, int): # type: ignore[unreachable] raise vol.Invalid("Make sure you wrap time values in quotes") if not isinstance(value, str): raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) @@ -585,7 +587,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 + template_value = template_helper.Template(str(value)) # type: ignore[no-untyped-call] try: template_value.ensure_valid() @@ -603,7 +605,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 + template_value = template_helper.Template(str(value)) # type: ignore[no-untyped-call] try: template_value.ensure_valid() return template_value @@ -796,7 +798,7 @@ def validator(config: dict) -> dict: """Check if key is in config and log warning or error.""" if key in config: try: - near = f"near {config.__config_file__}:{config.__line__} " # type: ignore + near = f"near {config.__config_file__}:{config.__line__} " # type: ignore[attr-defined] except AttributeError: near = "" arguments: tuple[str, ...] @@ -937,6 +939,8 @@ def validator(value: dict[Hashable, Any]) -> dict[Hashable, Any]: def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" + from . import selector # pylint: disable=import-outside-toplevel + if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} @@ -949,6 +953,9 @@ def custom_serializer(schema: Any) -> Any: if isinstance(schema, multi_select): return {"type": "multi_select", "options": schema.options} + if isinstance(schema, selector.Selector): + return schema.serialize() + return voluptuous_serialize.UNSUPPORTED @@ -1026,7 +1033,12 @@ def script_action(value: Any) -> dict: if not isinstance(value, dict): raise vol.Invalid("expected dictionary") - return ACTION_TYPE_SCHEMAS[determine_script_action(value)](value) + try: + action = determine_script_action(value) + except ValueError as err: + raise vol.Invalid(str(err)) + + return ACTION_TYPE_SCHEMAS[action](value) SCRIPT_SCHEMA = vol.All(ensure_list, [script_action]) @@ -1058,6 +1070,8 @@ def script_action(value: Any) -> dict: ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, vol.Optional(CONF_TARGET): vol.Any(TARGET_SERVICE_FIELDS, dynamic_template), + # The frontend stores data here. Don't use in core. + vol.Remove("metadata"): dict, } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), @@ -1435,7 +1449,10 @@ def determine_script_action(action: dict[str, Any]) -> str: if CONF_VARIABLES in action: return SCRIPT_ACTION_VARIABLES - return SCRIPT_ACTION_CALL_SERVICE + if CONF_SERVICE in action or CONF_SERVICE_TEMPLATE in action: + return SCRIPT_ACTION_CALL_SERVICE + + raise ValueError("Unable to determine action") ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 09345bf51bfd1..07f5e640ea3f7 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -70,7 +70,7 @@ async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response try: result = await self._flow_mgr.async_init( - handler, # type: ignore + handler, # type: ignore[arg-type] context={ "source": config_entries.SOURCE_USER, "show_advanced_options": data["show_advanced_options"], diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index e3f13e3ad16c0..7937459b50cb8 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -97,7 +97,7 @@ async def _handle_timer_finish(self) -> None: async with self._execute_lock: # Abort if timer got set while we're waiting for the lock. if self._timer_task: - return # type: ignore + return # type: ignore[unreachable] try: task = self.hass.async_run_hass_job(self._job) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 96425b2ea93bc..cb009efeb074d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,7 +11,7 @@ from homeassistant.backports.enum import StrEnum from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import RequiredParameterMissing +from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util @@ -420,10 +420,14 @@ def async_update_device( """Update device attributes.""" old = self.devices[device_id] - changes: dict[str, Any] = {} + new_values: dict[str, Any] = {} # Dict with new key/value pairs + old_values: dict[str, Any] = {} # Dict with old key/value pairs config_entries = old.config_entries + if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: + raise HomeAssistantError() + if isinstance(disabled_by, str) and not isinstance( disabled_by, DeviceEntryDisabler ): @@ -462,7 +466,8 @@ def async_update_device( config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: - changes["config_entries"] = config_entries + new_values["config_entries"] = config_entries + old_values["config_entries"] = old.config_entries for attr_name, setvalue in ( ("connections", merge_connections), @@ -471,10 +476,12 @@ def async_update_device( old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. if setvalue is not UNDEFINED and not setvalue.issubset(old_value): - changes[attr_name] = old_value | setvalue + new_values[attr_name] = old_value | setvalue + old_values[attr_name] = old_value if new_identifiers is not UNDEFINED: - changes["identifiers"] = new_identifiers + new_values["identifiers"] = new_identifiers + old_values["identifiers"] = old.identifiers for attr_name, value in ( ("configuration_url", configuration_url), @@ -491,25 +498,27 @@ def async_update_device( ("via_device_id", via_device_id), ): if value is not UNDEFINED and value != getattr(old, attr_name): - changes[attr_name] = value + new_values[attr_name] = value + old_values[attr_name] = getattr(old, attr_name) if old.is_new: - changes["is_new"] = False + new_values["is_new"] = False - if not changes: + if not new_values: return old - new = attr.evolve(old, **changes) + new = attr.evolve(old, **new_values) self._update_device(old, new) self.async_schedule_save() - self.hass.bus.async_fire( - EVENT_DEVICE_REGISTRY_UPDATED, - { - "action": "create" if "is_new" in changes else "update", - "device_id": new.id, - }, - ) + data: dict[str, Any] = { + "action": "create" if old.is_new else "update", + "device_id": new.id, + } + if not old.is_new: + data["changes"] = old_values + + self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) return new diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index ed90b5b893b58..20819ac75047c 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -66,11 +66,7 @@ def discover( hass_config: ConfigType, ) -> None: """Fire discovery event. Can ensure a component is loaded.""" - hass.add_job( - async_discover( # type: ignore - hass, service, discovered, component, hass_config - ) - ) + hass.add_job(async_discover(hass, service, discovered, component, hass_config)) @bind_hass @@ -131,9 +127,7 @@ def load_platform( ) -> None: """Load a component and platform dynamically.""" hass.add_job( - async_load_platform( # type: ignore - hass, component, platform, discovered, hass_config - ) + async_load_platform(hass, component, platform, discovered, hass_config) ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e9038d1f65859..8e4f6bc8b5885 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -255,12 +255,12 @@ class Entity(ABC): # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. - entity_id: str = None # type: ignore + entity_id: str = None # type: ignore[assignment] # Owning hass instance. Will be set by EntityPlatform # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - hass: HomeAssistant = None # type: ignore + hass: HomeAssistant = None # type: ignore[assignment] # Owning platform instance. Will be set by EntityPlatform platform: EntityPlatform | None = None @@ -674,7 +674,7 @@ def schedule_update_ha_state(self, force_refresh: bool = False) -> None: If state is changed more than once before the ha state change task has been executed, the intermediate state transitions will be missed. """ - self.hass.add_job(self.async_update_ha_state(force_refresh)) # type: ignore + self.hass.add_job(self.async_update_ha_state(force_refresh)) @callback def async_schedule_update_ha_state(self, force_refresh: bool = False) -> None: @@ -707,10 +707,11 @@ async def async_device_update(self, warning: bool = True) -> None: await self.parallel_updates.acquire() try: + task: asyncio.Future[None] if hasattr(self, "async_update"): - task = self.hass.async_create_task(self.async_update()) # type: ignore + task = self.hass.async_create_task(self.async_update()) # type: ignore[attr-defined] elif hasattr(self, "update"): - task = self.hass.async_add_executor_job(self.update) # type: ignore + task = self.hass.async_add_executor_job(self.update) # type: ignore[attr-defined] else: return @@ -770,7 +771,7 @@ def add_to_platform_start( @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" - self.hass = None # type: ignore + self.hass = None # type: ignore[assignment] self.platform = None self.parallel_updates = None self._added = False diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index da6732d05e76f..a1dba0d69622f 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -105,7 +105,7 @@ def setup(self, config: ConfigType) -> None: This doesn't block the executor to protect from deadlocks. """ - self.hass.add_job(self.async_setup(config)) # type: ignore + self.hass.add_job(self.async_setup(config)) async def async_setup(self, config: ConfigType) -> None: """Set up a full entity component. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 799b209f16e3c..cc252f8278217 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -172,7 +172,7 @@ async def async_setup( def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): - return platform.async_setup_platform( # type: ignore + return platform.async_setup_platform( # type: ignore[no-any-return,union-attr] hass, platform_config, self._async_schedule_add_entities, @@ -183,7 +183,7 @@ def async_create_setup_task() -> Coroutine: # we don't want to track this task in case it blocks startup. return hass.loop.run_in_executor( # type: ignore[return-value] None, - platform.setup_platform, # type: ignore + platform.setup_platform, # type: ignore[union-attr] hass, platform_config, self._schedule_add_entities, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 36ac5cc3dde57..4d4fce6e68529 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -446,13 +446,31 @@ def async_device_modified(self, event: Event) -> None: return if event.data["action"] != "update": + # Ignore "create" action return device_registry = dr.async_get(self.hass) device = device_registry.async_get(event.data["device_id"]) - # The device may be deleted already if the event handling is late - if not device or not device.disabled: + # The device may be deleted already if the event handling is late, do nothing + # in that case. Entities will be removed when we get the "remove" event. + if not device: + return + + # Remove entities which belong to config entries no longer associated with the + # device + entities = async_entries_for_device( + self, event.data["device_id"], include_disabled_entities=True + ) + for entity in entities: + if ( + entity.config_entry_id is not None + and entity.config_entry_id not in device.config_entries + ): + self.async_remove(entity.entity_id) + + # Re-enable disabled entities if the device is no longer disabled + if not device.disabled: entities = async_entries_for_device( self, event.data["device_id"], include_disabled_entities=True ) @@ -462,11 +480,12 @@ def async_device_modified(self, event: Event) -> None: self.async_update_entity(entity.entity_id, disabled_by=None) return + # Ignore device disabled by config entry, this is handled by + # async_config_entry_disabled if device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY: - # Handled by async_config_entry_disabled return - # Fetch entities which are not already disabled + # Fetch entities which are not already disabled and disable them entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: self.async_update_entity( diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index cbafd2e7e959c..71b2cf1a58572 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1328,8 +1328,8 @@ def async_track_time_interval( interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" - remove = None - interval_listener_job = None + remove: CALLBACK_TYPE + interval_listener_job: HassJob[None] job = HassJob(action) @@ -1344,7 +1344,7 @@ def interval_listener(now: datetime) -> None: nonlocal interval_listener_job remove = async_track_point_in_utc_time( - hass, interval_listener_job, next_interval() # type: ignore + hass, interval_listener_job, next_interval() ) hass.async_run_hass_job(job, now) @@ -1353,7 +1353,7 @@ def interval_listener(now: datetime) -> None: def remove_listener() -> None: """Remove interval listener.""" - remove() # type: ignore + remove() return remove_listener diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index d1dc11aae4d2c..e2ebbd31dacd5 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -71,7 +71,7 @@ def create_async_httpx_client( original_aclose = client.aclose - client.aclose = warn_use( # type: ignore + client.aclose = warn_use( # type: ignore[assignment] client.aclose, "closes the Home Assistant httpx client" ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ca154d20b75ce..44dd21d7fa39c 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -152,7 +152,7 @@ def async_validate_slots(self, slots: _SlotsType) -> _SlotsType: extra=vol.ALLOW_EXTRA, ) - return self._slot_schema(slots) # type: ignore + return self._slot_schema(slots) # type: ignore[no-any-return] async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the intent.""" @@ -249,6 +249,7 @@ def __init__(self, intent: Intent | None = None) -> None: """Initialize an IntentResponse.""" self.intent = intent self.speech: dict[str, dict[str, Any]] = {} + self.reprompt: dict[str, dict[str, Any]] = {} self.card: dict[str, dict[str, str]] = {} @callback @@ -258,14 +259,25 @@ def async_set_speech( """Set speech response.""" self.speech[speech_type] = {"speech": speech, "extra_data": extra_data} + @callback + def async_set_reprompt( + self, speech: str, speech_type: str = "plain", extra_data: Any | None = None + ) -> None: + """Set reprompt response.""" + self.reprompt[speech_type] = {"reprompt": speech, "extra_data": extra_data} + @callback def async_set_card( self, title: str, content: str, card_type: str = "simple" ) -> None: - """Set speech response.""" + """Set card response.""" self.card[card_type] = {"title": title, "content": content} @callback def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]: """Return a dictionary representation of an intent response.""" - return {"speech": self.speech, "card": self.card} + return ( + {"speech": self.speech, "reprompt": self.reprompt, "card": self.card} + if self.reprompt + else {"speech": self.speech, "card": self.card} + ) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index a3f2dfb4c6fbd..bbc32145706ce 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -4,8 +4,6 @@ from collections.abc import Iterable import logging -import voluptuous as vol - from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant, State from homeassistant.util import location as loc_util @@ -48,29 +46,42 @@ def closest(latitude: float, longitude: float, states: Iterable[State]) -> State def find_coordinates( - hass: HomeAssistant, entity_id: str, recursion_history: list | None = None + hass: HomeAssistant, name: str, recursion_history: list | None = None ) -> str | None: - """Find the gps coordinates of the entity in the form of '90.000,180.000'.""" - if (entity_state := hass.states.get(entity_id)) is None: - _LOGGER.error("Unable to find entity %s", entity_id) - return None + """Try to resolve the a location from a supplied name or entity_id. + + Will recursively resolve an entity if pointed to by the state of the supplied entity. + Returns coordinates in the form of '90.000,180.000', an address or the state of the last resolved entity. + """ + # Check if a friendly name of a zone was supplied + if (zone_coords := resolve_zone(hass, name)) is not None: + return zone_coords + + # Check if an entity_id was supplied. + if (entity_state := hass.states.get(name)) is None: + _LOGGER.debug("Unable to find entity %s", name) + return name - # Check if the entity has location attributes + # Check if the entity_state has location attributes if has_location(entity_state): return _get_location_from_attributes(entity_state) - # Check if device is in a zone + # Check if entity_state is a zone zone_entity = hass.states.get(f"zone.{entity_state.state}") - if has_location(zone_entity): # type: ignore + if has_location(zone_entity): # type: ignore[arg-type] _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_entity.entity_id # type: ignore + "%s is in %s, getting zone location", name, zone_entity.entity_id # type: ignore[union-attr] ) - return _get_location_from_attributes(zone_entity) # type: ignore + return _get_location_from_attributes(zone_entity) # type: ignore[arg-type] + + # Check if entity_state is a friendly name of a zone + if (zone_coords := resolve_zone(hass, entity_state.state)) is not None: + return zone_coords - # Resolve nested entity + # Check if entity_state is an entity_id if recursion_history is None: recursion_history = [] - recursion_history.append(entity_id) + recursion_history.append(name) if entity_state.state in recursion_history: _LOGGER.error( "Circular reference detected while trying to find coordinates of an entity. The state of %s has already been checked", @@ -83,21 +94,18 @@ def find_coordinates( _LOGGER.debug("Resolving nested entity_id: %s", entity_state.state) return find_coordinates(hass, entity_state.state, recursion_history) - # Check if state is valid coordinate set - try: - # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + # Might be an address, coordinates or anything else. This has to be checked by the caller. + return entity_state.state - cv.gps(entity_state.state.split(",")) - except vol.Invalid: - _LOGGER.error( - "Entity %s does not contain a location and does not point at an entity that does: %s", - entity_id, - entity_state.state, - ) - return None - else: - return entity_state.state + +def resolve_zone(hass: HomeAssistant, zone_name: str) -> str | None: + """Get a lat/long from a zones friendly_name or None if no zone is found by that friendly_name.""" + states = hass.states.async_all("zone") + for state in states: + if state.name == zone_name: + return _get_location_from_attributes(state) + + return None def _get_location_from_attributes(entity_state: State) -> str: diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 0b2804f821df1..a8c4b3cf45875 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -25,12 +25,56 @@ class NoURLAvailableError(HomeAssistantError): def is_internal_request(hass: HomeAssistant) -> bool: """Test if the current request is internal.""" try: - _get_internal_url(hass, require_current_request=True) + get_url( + hass, allow_external=False, allow_cloud=False, require_current_request=True + ) return True except NoURLAvailableError: return False +def is_hass_url(hass: HomeAssistant, url: str) -> bool: + """Return if the URL points at this Home Assistant instance.""" + parsed = yarl.URL(normalize_url(url)) + + def host_ip() -> str | None: + if hass.config.api is None or is_loopback(ip_address(hass.config.api.local_ip)): + return None + + return str( + yarl.URL.build( + scheme="http", host=hass.config.api.local_ip, port=hass.config.api.port + ) + ) + + def cloud_url() -> str | None: + try: + return _get_cloud_url(hass) + except NoURLAvailableError: + return None + + for potential_base_factory in ( + lambda: hass.config.internal_url, + lambda: hass.config.external_url, + cloud_url, + host_ip, + ): + potential_base = potential_base_factory() + + if potential_base is None: + continue + + potential_parsed = yarl.URL(normalize_url(potential_base)) + + if ( + parsed.scheme == potential_parsed.scheme + and parsed.authority == potential_parsed.authority + ): + return True + + return False + + @bind_hass def get_url( hass: HomeAssistant, @@ -41,14 +85,20 @@ def get_url( allow_internal: bool = True, allow_external: bool = True, allow_cloud: bool = True, - allow_ip: bool = True, - prefer_external: bool = False, + allow_ip: bool | None = None, + prefer_external: bool | None = None, prefer_cloud: bool = False, ) -> str: """Get a URL to this instance.""" if require_current_request and http.current_request.get() is None: raise NoURLAvailableError + if prefer_external is None: + prefer_external = hass.config.api is not None and hass.config.api.use_ssl + + if allow_ip is None: + allow_ip = hass.config.api is None or not hass.config.api.use_ssl + order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL] if prefer_external: order.reverse() diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4857210f125ed..b8262d3a53320 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,6 +1,7 @@ """Support for restoring entity states on startup.""" from __future__ import annotations +from abc import abstractmethod import asyncio from datetime import datetime, timedelta import logging @@ -34,27 +35,65 @@ _StoredStateT = TypeVar("_StoredStateT", bound="StoredState") +class ExtraStoredData: + """Object to hold extra stored data.""" + + @abstractmethod + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the extra data. + + Must be serializable by Home Assistant's JSONEncoder. + """ + + +class RestoredExtraData(ExtraStoredData): + """Object to hold extra stored data loaded from storage.""" + + def __init__(self, json_dict: dict[str, Any]) -> None: + """Object to hold extra stored data.""" + self.json_dict = json_dict + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the extra data.""" + return self.json_dict + + class StoredState: """Object to represent a stored state.""" - def __init__(self, state: State, last_seen: datetime) -> None: + def __init__( + self, + state: State, + extra_data: ExtraStoredData | None, + last_seen: datetime, + ) -> None: """Initialize a new stored state.""" - self.state = state + self.extra_data = extra_data self.last_seen = last_seen + self.state = state def as_dict(self) -> dict[str, Any]: """Return a dict representation of the stored state.""" - return {"state": self.state.as_dict(), "last_seen": self.last_seen} + result = { + "state": self.state.as_dict(), + "extra_data": self.extra_data.as_dict() if self.extra_data else None, + "last_seen": self.last_seen, + } + return result @classmethod def from_dict(cls: type[_StoredStateT], json_dict: dict) -> _StoredStateT: """Initialize a stored state from a dict.""" + extra_data_dict = json_dict.get("extra_data") + extra_data = RestoredExtraData(extra_data_dict) if extra_data_dict else None last_seen = json_dict["last_seen"] if isinstance(last_seen, str): last_seen = dt_util.parse_datetime(last_seen) - return cls(cast(State, State.from_dict(json_dict["state"])), last_seen) + return cls( + cast(State, State.from_dict(json_dict["state"])), extra_data, last_seen + ) class RestoreStateData: @@ -104,7 +143,7 @@ def __init__(self, hass: HomeAssistant) -> None: hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder ) self.last_states: dict[str, StoredState] = {} - self.entity_ids: set[str] = set() + self.entities: dict[str, RestoreEntity] = {} @callback def async_get_stored_states(self) -> list[StoredState]: @@ -125,9 +164,11 @@ def async_get_stored_states(self) -> list[StoredState]: # Start with the currently registered states stored_states = [ - StoredState(state, now) + StoredState( + state, self.entities[state.entity_id].extra_restore_state_data, now + ) for state in all_states - if state.entity_id in self.entity_ids and + if state.entity_id in self.entities and # Ignore all states that are entity registry placeholders not state.attributes.get(ATTR_RESTORED) ] @@ -188,12 +229,14 @@ async def _async_dump_states_at_stop(*_: Any) -> None: ) @callback - def async_restore_entity_added(self, entity_id: str) -> None: + def async_restore_entity_added(self, entity: RestoreEntity) -> None: """Store this entity's state when hass is shutdown.""" - self.entity_ids.add(entity_id) + self.entities[entity.entity_id] = entity @callback - def async_restore_entity_removed(self, entity_id: str) -> None: + def async_restore_entity_removed( + self, entity_id: str, extra_data: ExtraStoredData | None + ) -> None: """Unregister this entity from saving state.""" # When an entity is being removed from hass, store its last state. This # allows us to support state restoration if the entity is removed, then @@ -204,16 +247,18 @@ def async_restore_entity_removed(self, entity_id: str) -> None: if state is not None: state = State.from_dict(_encode_complex(state.as_dict())) if state is not None: - self.last_states[entity_id] = StoredState(state, dt_util.utcnow()) + self.last_states[entity_id] = StoredState( + state, extra_data, dt_util.utcnow() + ) - self.entity_ids.remove(entity_id) + self.entities.pop(entity_id) def _encode(value: Any) -> Any: """Little helper to JSON encode a value.""" try: return JSONEncoder.default( - None, # type: ignore + None, # type: ignore[arg-type] value, ) except TypeError: @@ -244,7 +289,7 @@ async def async_internal_added_to_hass(self) -> None: super().async_internal_added_to_hass(), RestoreStateData.async_get_instance(self.hass), ) - data.async_restore_entity_added(self.entity_id) + data.async_restore_entity_added(self) async def async_internal_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" @@ -252,10 +297,10 @@ async def async_internal_will_remove_from_hass(self) -> None: super().async_internal_will_remove_from_hass(), RestoreStateData.async_get_instance(self.hass), ) - data.async_restore_entity_removed(self.entity_id) + data.async_restore_entity_removed(self.entity_id, self.extra_restore_state_data) - async def async_get_last_state(self) -> State | None: - """Get the entity state from the previous run.""" + async def _async_get_restored_data(self) -> StoredState | None: + """Get data stored for an entity, if any.""" if self.hass is None or self.entity_id is None: # Return None if this entity isn't added to hass yet _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] @@ -265,4 +310,24 @@ async def async_get_last_state(self) -> State | None: ) if self.entity_id not in data.last_states: return None - return data.last_states[self.entity_id].state + return data.last_states[self.entity_id] + + async def async_get_last_state(self) -> State | None: + """Get the entity state from the previous run.""" + if (stored_state := await self._async_get_restored_data()) is None: + return None + return stored_state.state + + async def async_get_last_extra_data(self) -> ExtraStoredData | None: + """Get the entity specific state data from the previous run.""" + if (stored_state := await self._async_get_restored_data()) is None: + return None + return stored_state.extra_data + + @property + def extra_restore_state_data(self) -> ExtraStoredData | None: + """Return entity specific state data to be restored. + + Implemented by platform classes. + """ + return None diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 5a80691fa4615..1eabc33b89d20 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -15,7 +15,8 @@ import voluptuous as vol from homeassistant import exceptions -from homeassistant.components import device_automation, scene +from homeassistant.components import scene +from homeassistant.components.device_automation import action as device_action from homeassistant.components.logger import LOGSEVERITY from homeassistant.const import ( ATTR_AREA_ID, @@ -244,13 +245,7 @@ async def async_validate_action_config( pass elif action_type == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: - platform = await device_automation.async_get_device_automation_platform( - hass, config[CONF_DOMAIN], device_automation.DeviceAutomationType.ACTION - ) - if hasattr(platform, "async_validate_action_config"): - config = await platform.async_validate_action_config(hass, config) - else: - config = platform.ACTION_SCHEMA(config) + config = await device_action.async_validate_action_config(hass, config) elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION: config = await condition.async_validate_condition_config(hass, config) @@ -580,12 +575,7 @@ async def _async_call_service_step(self): async def _async_device_step(self): """Perform the device automation specified in the action.""" self._step_log("device automation") - platform = await device_automation.async_get_device_automation_platform( - self._hass, - self._action[CONF_DOMAIN], - device_automation.DeviceAutomationType.ACTION, - ) - await platform.async_call_action_from_config( + await device_action.async_call_action_from_config( self._hass, self._action, self._variables, self._context ) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index f17b610ff23ee..f280feb83b283 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,29 +2,53 @@ from __future__ import annotations from collections.abc import Callable +from datetime import time as time_sys from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT +from homeassistant.core import split_entity_id from homeassistant.util import decorator -SELECTORS = decorator.Registry() +from . import config_validation as cv +SELECTORS: decorator.Registry[str, type[Selector]] = decorator.Registry() -def validate_selector(config: Any) -> dict: - """Validate a selector.""" + +def _get_selector_class(config: Any) -> type[Selector]: + """Get selector class type.""" if not isinstance(config, dict): raise vol.Invalid("Expected a dictionary") if len(config) != 1: raise vol.Invalid(f"Only one type can be specified. Found {', '.join(config)}") - selector_type = list(config)[0] + selector_type: str = list(config)[0] if (selector_class := SELECTORS.get(selector_type)) is None: raise vol.Invalid(f"Unknown selector type {selector_type} found") + return selector_class + + +def selector(config: Any) -> Selector: + """Instantiate a selector.""" + selector_class = _get_selector_class(config) + selector_type = list(config)[0] + + # Selectors can be empty + if config[selector_type] is None: + return selector_class({selector_type: {}}) + + return selector_class(config) + + +def validate_selector(config: Any) -> dict: + """Validate a selector.""" + selector_class = _get_selector_class(config) + selector_type = list(config)[0] + # Selectors can be empty if config[selector_type] is None: return {selector_type: {}} @@ -38,12 +62,24 @@ class Selector: """Base class for selectors.""" CONFIG_SCHEMA: Callable + config: Any + selector_type: str + + def __init__(self, config: Any) -> None: + """Instantiate a selector.""" + self.config = self.CONFIG_SCHEMA(config[self.selector_type]) + + def serialize(self) -> Any: + """Serialize Selector for voluptuous_serialize.""" + return {"selector": {self.selector_type: self.config}} @SELECTORS.register("entity") class EntitySelector(Selector): """Selector of a single entity.""" + selector_type = "entity" + CONFIG_SCHEMA = vol.Schema( { # Integration that provided the entity @@ -55,11 +91,30 @@ class EntitySelector(Selector): } ) + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + try: + entity_id = cv.entity_id(data) + domain = split_entity_id(entity_id)[0] + except vol.Invalid: + # Not a valid entity_id, maybe it's an entity entry id + return cv.entity_id_or_uuid(cv.string(data)) + else: + if "domain" in self.config and domain != self.config["domain"]: + raise vol.Invalid( + f"Entity {entity_id} belongs to domain {domain}, " + f"expected {self.config['domain']}" + ) + + return entity_id + @SELECTORS.register("device") class DeviceSelector(Selector): """Selector of a single device.""" + selector_type = "device" + CONFIG_SCHEMA = vol.Schema( { # Integration linked to it with a config entry @@ -73,35 +128,35 @@ class DeviceSelector(Selector): } ) + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + return cv.string(data) + @SELECTORS.register("area") class AreaSelector(Selector): """Selector of a single area.""" + selector_type = "area" + CONFIG_SCHEMA = vol.Schema( { - vol.Optional("entity"): vol.Schema( - { - vol.Optional("domain"): str, - vol.Optional("device_class"): str, - vol.Optional("integration"): str, - } - ), - vol.Optional("device"): vol.Schema( - { - vol.Optional("integration"): str, - vol.Optional("manufacturer"): str, - vol.Optional("model"): str, - } - ), + vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, + vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, } ) + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + return cv.string(data) + @SELECTORS.register("number") class NumberSelector(Selector): """Selector of a numeric value.""" + selector_type = "number" + CONFIG_SCHEMA = vol.Schema( { vol.Required("min"): vol.Coerce(float), @@ -114,80 +169,131 @@ class NumberSelector(Selector): } ) + def __call__(self, data: Any) -> float: + """Validate the passed selection.""" + value: float = vol.Coerce(float)(data) + + if not self.config["min"] <= value <= self.config["max"]: + raise vol.Invalid(f"Value {value} is too small or too large") + + return value + @SELECTORS.register("addon") class AddonSelector(Selector): """Selector of a add-on.""" + selector_type = "addon" + CONFIG_SCHEMA = vol.Schema({}) + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + return cv.string(data) + @SELECTORS.register("boolean") class BooleanSelector(Selector): """Selector of a boolean value.""" + selector_type = "boolean" + CONFIG_SCHEMA = vol.Schema({}) + def __call__(self, data: Any) -> bool: + """Validate the passed selection.""" + value: bool = vol.Coerce(bool)(data) + return value + @SELECTORS.register("time") class TimeSelector(Selector): """Selector of a time value.""" + selector_type = "time" + CONFIG_SCHEMA = vol.Schema({}) + def __call__(self, data: Any) -> time_sys: + """Validate the passed selection.""" + return cv.time(data) + @SELECTORS.register("target") class TargetSelector(Selector): """Selector of a target value (area ID, device ID, entity ID etc). - Value should follow cv.ENTITY_SERVICE_FIELDS format. + Value should follow cv.TARGET_SERVICE_FIELDS format. """ + selector_type = "target" + CONFIG_SCHEMA = vol.Schema( { - vol.Optional("entity"): vol.Schema( - { - vol.Optional("domain"): str, - vol.Optional("device_class"): str, - vol.Optional("integration"): str, - } - ), - vol.Optional("device"): vol.Schema( - { - vol.Optional("integration"): str, - vol.Optional("manufacturer"): str, - vol.Optional("model"): str, - } - ), + vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, + vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, } ) + TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) + + def __call__(self, data: Any) -> dict[str, list[str]]: + """Validate the passed selection.""" + target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) + return target + @SELECTORS.register("action") class ActionSelector(Selector): """Selector of an action sequence (script syntax).""" + selector_type = "action" + CONFIG_SCHEMA = vol.Schema({}) + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + return data + @SELECTORS.register("object") class ObjectSelector(Selector): """Selector for an arbitrary object.""" + selector_type = "object" + CONFIG_SCHEMA = vol.Schema({}) + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + return data + @SELECTORS.register("text") class StringSelector(Selector): """Selector for a multi-line text string.""" + selector_type = "text" + CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool}) + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + text = cv.string(data) + return text + @SELECTORS.register("select") class SelectSelector(Selector): """Selector for an single-choice input select.""" + selector_type = "select" + CONFIG_SCHEMA = vol.Schema( {vol.Required("options"): vol.All([str], vol.Length(min=1))} ) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + selected_option = vol.In(self.config["options"])(cv.string(data)) + return selected_option diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3cf11453e20f1..e638288a58cec 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -485,7 +485,7 @@ async def async_get_all_descriptions( # Cache missing descriptions if description is None: domain_yaml = loaded[domain] - yaml_description = domain_yaml.get(service, {}) # type: ignore + yaml_description = domain_yaml.get(service, {}) # type: ignore[union-attr] # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service @@ -696,7 +696,7 @@ async def _handle_entity_call( entity.async_set_context(context) if isinstance(func, str): - result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore + result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] else: result = hass.async_run_job(func, entity, data) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 38647792b7a0c..75ca96ea2468f 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -102,12 +102,12 @@ async def worker(domain: str, states_by_domain: list[State]) -> None: return try: - platform: ModuleType | None = integration.get_platform("reproduce_state") + platform: ModuleType = integration.get_platform("reproduce_state") except ImportError: _LOGGER.warning("Integration %s does not support reproduce state", domain) return - await platform.async_reproduce_states( # type: ignore + await platform.async_reproduce_states( hass, states_by_domain, context=context, reproduce_options=reproduce_options ) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index af0c50ec5fa2a..554a88f4ad565 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -194,7 +194,11 @@ async def async_save(self, data: dict | list) -> None: await self._async_handle_write_data() @callback - def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None: + def async_delay_save( + self, + data_func: Callable[[], dict | list], + delay: float = 0, + ) -> None: """Save data with an optional delay.""" # pylint: disable-next=import-outside-toplevel from .event import async_call_later diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 3c18dcc32784a..09a329cd2752b 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -116,7 +116,7 @@ def get_astral_event_date( kwargs["observer_elevation"] = elevation try: - return getattr(location, event)(date, **kwargs) # type: ignore + return getattr(location, event)(date, **kwargs) # type: ignore[no-any-return] except ValueError: # Event never occurs for specified date. return None diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 916d203782ab3..2b93b69fe3795 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -836,7 +836,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 + raise TemplateError(f"Invalid entity ID '{entity_id}'") # type: ignore[arg-type] return _get_template_state_from_state(hass, entity_id, state) @@ -1302,7 +1302,7 @@ def forgiving_round(value, precision=0, method="common", default=_SENTINEL): """Filter to round a value.""" try: # support rounding methods like jinja - multiplier = float(10 ** precision) + multiplier = float(10**precision) if method == "ceil": value = math.ceil(float(value) * multiplier) / multiplier elif method == "floor": diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 2f20e1404d81a..976c66dda56ac 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -3,6 +3,7 @@ import asyncio from collections import ChainMap +from collections.abc import Mapping import logging from typing import Any @@ -134,7 +135,7 @@ def _build_resources( translation_strings: dict[str, dict[str, Any]], components: set[str], category: str, -) -> dict[str, dict[str, Any]]: +) -> dict[str, dict[str, Any] | str]: """Build the resources response for the given components.""" # Build response return { @@ -251,6 +252,7 @@ def _build_category_cache( translation_strings: dict[str, dict[str, Any]], ) -> None: """Extract resources into the cache.""" + resource: dict[str, Any] | str cached = self.cache.setdefault(language, {}) categories: set[str] = set() for resource in translation_strings.values(): @@ -260,7 +262,8 @@ def _build_category_cache( resource_func = ( _merge_resources if category == "state" else _build_resources ) - new_resources = resource_func(translation_strings, components, category) + new_resources: Mapping[str, dict[str, Any] | str] + new_resources = resource_func(translation_strings, components, category) # type: ignore[assignment] for component, resource in new_resources.items(): category_cache: dict[str, Any] = cached.setdefault( diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 175a29fcc5da1..0b18ad9aa421d 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -83,7 +83,7 @@ async def async_initialize_triggers( triggers.append(platform.async_attach_trigger(hass, conf, action, info)) attach_results = await asyncio.gather(*triggers, return_exceptions=True) - removes = [] + removes: list[Callable[[], None]] = [] for result in attach_results: if isinstance(result, HomeAssistantError): @@ -103,7 +103,7 @@ async def async_initialize_triggers( log_cb(logging.INFO, "Initialized trigger") @callback - def remove_triggers(): # type: ignore + def remove_triggers() -> None: """Remove triggers.""" for remove in removes: remove() diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 04ddd8df571fe..f5c68897e2e25 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -82,12 +82,13 @@ class Manifest(TypedDict, total=False): mqtt: list[str] ssdp: list[dict[str, str]] zeroconf: list[str | dict[str, str]] - dhcp: list[dict[str, str]] + dhcp: list[dict[str, bool | str]] usb: list[dict[str, str]] homekit: dict[str, list[str]] is_built_in: bool version: str codeowners: list[str] + loggers: list[str] def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -227,9 +228,9 @@ async def async_get_zeroconf( return zeroconf -async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str]]: +async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str | bool]]: """Return cached list of dhcp types.""" - dhcp: list[dict[str, str]] = DHCP.copy() + dhcp: list[dict[str, str | bool]] = DHCP.copy() integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -442,6 +443,11 @@ def issue_tracker(self) -> str | None: """Return issue tracker link.""" return self.manifest.get("issue_tracker") + @property + def loggers(self) -> list[str] | None: + """Return list of loggers used by the integration.""" + return self.manifest.get("loggers") + @property def quality_scale(self) -> str | None: """Return Integration Quality Scale.""" @@ -468,7 +474,7 @@ def zeroconf(self) -> list[str | dict[str, str]] | None: return self.manifest.get("zeroconf") @property - def dhcp(self) -> list[dict[str, str]] | None: + def dhcp(self) -> list[dict[str, str | bool]] | None: """Return Integration dhcp entries.""" return self.manifest.get("dhcp") @@ -675,7 +681,7 @@ def _load_file( Async friendly. """ with suppress(KeyError): - return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore + return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore[no-any-return] if (cache := hass.data.get(DATA_COMPONENTS)) is None: if not _async_mount_config_dir(hass): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0bf79dafd6f10..bd9e1da6a69e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,27 +1,26 @@ PyJWT==2.1.0 PyNaCl==1.4.0 -aiodiscover==1.4.7 +aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.4 +async-upnp-client==0.23.5 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==22.1.0 +awesomeversion==22.2.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 -emoji==1.6.3 -hass-nabucasa==0.52.0 -home-assistant-frontend==20220203.1 +hass-nabucasa==0.54.0 +home-assistant-frontend==20220301.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 pillow==9.0.1 -pip>=8.0.3,<20.3 +pip>=21.0,<22.1 pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 @@ -33,7 +32,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.7.2 -zeroconf==0.38.3 +zeroconf==0.38.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 @@ -49,7 +48,7 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.43.0 +grpcio==1.44.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, @@ -67,10 +66,6 @@ enum34==1000000000.0.0 typing==1000000000.0.0 uuid==1000000000.0.0 -# Temporary constraint on pandas, to unblock 2021.7 releases -# until we have fixed the wheels builds for newer versions. -pandas==1.3.0 - # regex causes segfault with version 2021.8.27 # https://bitbucket.org/mrabarnett/mrab-regex/issues/421/2021827-results-in-fatal-python-error # This is fixed in 2021.8.28 @@ -84,6 +79,10 @@ anyio==3.5.0 h11==0.12.0 httpcore==0.14.5 +# Ensure we have a hyperframe version that works in Python 3.10 +# 5.2.0 fixed a collections abc deprecation +hyperframe>=5.2.0 + # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 @@ -94,5 +93,5 @@ python-engineio>=3.13.1,<4.0 python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues -# https://github.com/home-assistant/core/pull/64792 -multidict<6.0.0 +# https://github.com/home-assistant/core/pull/67046 +multidict>=6.0.2 diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 571111d107728..472d399713dba 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -15,8 +15,8 @@ from .util.thread import deadlock_safe_shutdown # -# Python 3.8 has significantly less workers by default -# than Python 3.7. In order to be consistent between +# Some Python versions may have different number of workers by default +# than others. In order to be consistent between # supported versions, we need to set max_workers. # # In most cases the workers are not I/O bound, as they @@ -59,7 +59,7 @@ def __init__(self, debug: bool) -> None: @property def loop_name(self) -> str: """Return name of the loop.""" - return self._loop_factory.__name__ # type: ignore + return self._loop_factory.__name__ # type: ignore[no-any-return] def new_event_loop(self) -> asyncio.AbstractEventLoop: """Get the event loop.""" @@ -72,7 +72,7 @@ def new_event_loop(self) -> asyncio.AbstractEventLoop: thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS ) loop.set_default_executor(executor) - loop.set_default_executor = warn_use( # type: ignore + loop.set_default_executor = warn_use( # type: ignore[assignment] loop.set_default_executor, "sets default executor on the event loop" ) return loop @@ -89,11 +89,11 @@ def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: if source_traceback := context.get("source_traceback"): stack_summary = "".join(traceback.format_list(source_traceback)) logger.error( - "Error doing job: %s: %s", context["message"], stack_summary, **kwargs # type: ignore + "Error doing job: %s: %s", context["message"], stack_summary, **kwargs # type: ignore[arg-type] ) return - logger.error("Error doing job: %s", context["message"], **kwargs) # type: ignore + logger.error("Error doing job: %s", context["message"], **kwargs) # type: ignore[arg-type] async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: @@ -121,9 +121,7 @@ def run(runtime_config: RuntimeConfig) -> int: try: _cancel_all_tasks_with_timeout(loop, TASK_CANCELATION_TIMEOUT) loop.run_until_complete(loop.shutdown_asyncgens()) - # Once cpython 3.8 is no longer supported we can use the - # the built-in loop.shutdown_default_executor - loop.run_until_complete(_shutdown_default_executor(loop)) + loop.run_until_complete(loop.shutdown_default_executor()) finally: asyncio.set_event_loop(None) loop.close() @@ -159,22 +157,3 @@ def _cancel_all_tasks_with_timeout( "task": task, } ) - - -async def _shutdown_default_executor(loop: asyncio.AbstractEventLoop) -> None: - """Backport of cpython 3.9 schedule the shutdown of the default executor.""" - future = loop.create_future() - - def _do_shutdown() -> None: - try: - loop._default_executor.shutdown(wait=True) # type: ignore # pylint: disable=protected-access - loop.call_soon_threadsafe(future.set_result, None) - except Exception as ex: # pylint: disable=broad-except - loop.call_soon_threadsafe(future.set_exception, ex) - - thread = threading.Thread(target=_do_shutdown) - thread.start() - try: - await future - finally: - thread.join() diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 1005b48e1ca7b..c57d082de61e1 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -65,7 +65,7 @@ async def fire_events(hass): """Fire a million events.""" count = 0 event_name = "benchmark_event" - events_to_fire = 10 ** 6 + events_to_fire = 10**6 @core.callback def listener(_): @@ -92,7 +92,7 @@ async def fire_events_with_filter(hass): """Fire a million events with a filter that rejects them.""" count = 0 event_name = "benchmark_event" - events_to_fire = 10 ** 6 + events_to_fire = 10**6 @core.callback def event_filter(event): @@ -131,13 +131,13 @@ def listener(_): nonlocal count count += 1 - if count == 10 ** 6: + if count == 10**6: event.set() hass.helpers.event.async_track_time_change(listener, minute=0, second=0) event_data = {ATTR_NOW: datetime(2017, 10, 10, 15, 0, 0, tzinfo=dt_util.UTC)} - for _ in range(10 ** 6): + for _ in range(10**6): hass.bus.async_fire(EVENT_TIME_CHANGED, event_data) start = timer() @@ -160,7 +160,7 @@ def listener(*args): nonlocal count count += 1 - if count == 10 ** 6: + if count == 10**6: event.set() for idx in range(1000): @@ -173,7 +173,7 @@ def listener(*args): "new_state": core.State(entity_id, "on"), } - for _ in range(10 ** 6): + for _ in range(10**6): hass.bus.async_fire(EVENT_STATE_CHANGED, event_data) start = timer() @@ -188,7 +188,7 @@ async def state_changed_event_helper(hass): """Run a million events through state changed event helper with 1000 entities.""" count = 0 entity_id = "light.kitchen" - events_to_fire = 10 ** 6 + events_to_fire = 10**6 @core.callback def listener(*args): @@ -223,7 +223,7 @@ async def state_changed_event_filter_helper(hass): """Run a million events through state changed event helper with 1000 entities that all get filtered.""" count = 0 entity_id = "light.kitchen" - events_to_fire = 10 ** 6 + events_to_fire = 10**6 @core.callback def listener(*args): @@ -292,7 +292,7 @@ async def _logbook_filtering(hass, last_changed, last_updated): ) def yield_events(event): - for _ in range(10 ** 5): + for _ in range(10**5): # pylint: disable=protected-access if logbook._keep_event(hass, event, entities_filter): yield event @@ -363,7 +363,7 @@ async def filtering_entity_id(hass): start = timer() - for i in range(10 ** 5): + for i in range(10**5): entities_filter(entity_ids[i % size]) return timer() - start @@ -373,7 +373,7 @@ async def filtering_entity_id(hass): async def valid_entity_id(hass): """Run valid entity ID a million times.""" start = timer() - for _ in range(10 ** 6): + for _ in range(10**6): core.valid_entity_id("light.kitchen") return timer() - start @@ -383,7 +383,7 @@ async def json_serialize_states(hass): """Serialize million states with websocket default encoder.""" states = [ core.State("light.kitchen", "on", {"friendly_name": "Kitchen Lights"}) - for _ in range(10 ** 6) + for _ in range(10**6) ] start = timer() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5c56cb55b1990..36292989dce9b 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import Awaitable, Callable, Generator, Iterable import contextlib +from datetime import timedelta import logging.handlers from timeit import default_timer as timer from types import ModuleType @@ -65,10 +66,10 @@ async def async_setup_component( if domain in hass.config.components: return True - setup_tasks = hass.data.setdefault(DATA_SETUP, {}) + setup_tasks: dict[str, asyncio.Task[bool]] = hass.data.setdefault(DATA_SETUP, {}) if domain in setup_tasks: - return await setup_tasks[domain] # type: ignore + return await setup_tasks[domain] task = setup_tasks[domain] = hass.async_create_task( _async_setup_component(hass, domain, config) @@ -436,7 +437,7 @@ def async_start_setup( """Keep track of when setup starts and finishes.""" setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) started = dt_util.utcnow() - unique_components = {} + unique_components: dict[str, str] = {} for domain in components: unique = ensure_unique_string(domain, setup_started) unique_components[unique] = domain @@ -444,7 +445,7 @@ def async_start_setup( yield - setup_time = hass.data.setdefault(DATA_SETUP_TIME, {}) + setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {}) time_taken = dt_util.utcnow() - started for unique, domain in unique_components.items(): del setup_started[unique] diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 3c82639251a8b..15e9254e9d875 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -2,14 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable, KeysView +from collections.abc import Callable, Coroutine, Iterable, KeysView, Mapping from datetime import datetime, timedelta from functools import wraps import random import re import string import threading -from types import MappingProxyType from typing import Any, TypeVar import slugify as unicode_slug @@ -53,7 +52,7 @@ def slugify(text: str | None, *, separator: str = "_") -> str: def repr_helper(inp: Any) -> str: """Help creating a more readable string representation of objects.""" - if isinstance(inp, (dict, MappingProxyType)): + if isinstance(inp, Mapping): return ", ".join( f"{repr_helper(key)}={repr_helper(item)}" for key, item in inp.items() ) @@ -138,7 +137,7 @@ async def throttled_value() -> None: else: - def throttled_value() -> None: # type: ignore + def throttled_value() -> None: # type: ignore[misc] """Stand-in function for when real func is being throttled.""" return None @@ -192,7 +191,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Callable | Coroutine: if force or utcnow() - throttle[1] > self.min_time: result = method(*args, **kwargs) throttle[1] = utcnow() - return result # type: ignore + return result # type: ignore[no-any-return] return throttled_value() finally: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8f9526b680069..b27ddbda3827a 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -11,14 +11,20 @@ from traceback import extract_stack from typing import Any, TypeVar +from typing_extensions import ParamSpec + _LOGGER = logging.getLogger(__name__) _SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" -T = TypeVar("T") +_T = TypeVar("_T") +_R = TypeVar("_R") +_P = ParamSpec("_P") -def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: +def fire_coroutine_threadsafe( + coro: Coroutine[Any, Any, Any], loop: AbstractEventLoop +) -> None: """Submit a coroutine object to a given event loop. This method does not provide a way to retrieve the result and @@ -40,8 +46,8 @@ def callback() -> None: def run_callback_threadsafe( - loop: AbstractEventLoop, callback: Callable[..., T], *args: Any -) -> concurrent.futures.Future[T]: + loop: AbstractEventLoop, callback: Callable[..., _T], *args: Any +) -> concurrent.futures.Future[_T]: """Submit a callback object to a given event loop. Return a concurrent.futures.Future to access the result. @@ -50,7 +56,7 @@ def run_callback_threadsafe( if ident is not None and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") - future: concurrent.futures.Future = concurrent.futures.Future() + future: concurrent.futures.Future[_T] = concurrent.futures.Future() def run_callback() -> None: """Run callback and store result.""" @@ -88,7 +94,7 @@ def run_callback() -> None: return future -def check_loop(func: Callable, strict: bool = True) -> None: +def check_loop(func: Callable[..., Any], strict: bool = True) -> None: """Warn if called inside the event loop. Raise if `strict` is True.""" try: get_running_loop() @@ -127,7 +133,7 @@ def check_loop(func: Callable, strict: bool = True) -> None: # Did not source from integration? Hard error. if found_frame is None: raise RuntimeError( - "Detected blocking call inside the event loop. " + f"Detected blocking call to {func.__name__} inside the event loop. " "This is causing stability issues. Please report issue" ) @@ -142,8 +148,9 @@ def check_loop(func: Callable, strict: bool = True) -> None: extra = "" _LOGGER.warning( - "Detected blocking call inside the event loop. This is causing stability issues. " + "Detected blocking call to %s inside the event loop. This is causing stability issues. " "Please report issue%s for %s doing blocking calls at %s, line %s: %s", + func.__name__, extra, integration, found_frame.filename[index:], @@ -158,11 +165,11 @@ def check_loop(func: Callable, strict: bool = True) -> None: ) -def protect_loop(func: Callable, strict: bool = True) -> Callable: +def protect_loop(func: Callable[_P, _R], strict: bool = True) -> Callable[_P, _R]: """Protect function from running in event loop.""" @functools.wraps(func) - def protected_loop_func(*args, **kwargs): # type: ignore + def protected_loop_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: check_loop(func, strict=strict) return func(*args, **kwargs) diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index 602cdba559830..c648f6f1caba4 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -2,18 +2,19 @@ from __future__ import annotations from collections.abc import Callable, Hashable -from typing import TypeVar +from typing import Any, TypeVar -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_KT = TypeVar("_KT", bound=Hashable) +_VT = TypeVar("_VT", bound=Callable[..., Any]) -class Registry(dict): +class Registry(dict[_KT, _VT]): """Registry of items.""" - def register(self, name: Hashable) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: _KT) -> Callable[[_VT], _VT]: """Return decorator to register item with a specific name.""" - def decorator(func: CALLABLE_T) -> CALLABLE_T: + def decorator(func: _VT) -> _VT: """Register decorated function.""" self[name] = func return func diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 5a8f15f434f6e..145c2ba4f0490 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -4,11 +4,11 @@ from concurrent.futures import ThreadPoolExecutor import contextlib import logging -import queue import sys from threading import Thread import time import traceback +from typing import Any from .thread import async_raise @@ -62,29 +62,9 @@ def join_or_interrupt_threads( class InterruptibleThreadPoolExecutor(ThreadPoolExecutor): """A ThreadPoolExecutor instance that will not deadlock on shutdown.""" - def shutdown(self, *args, **kwargs) -> None: # type: ignore - """Shutdown backport from cpython 3.9 with interrupt support added.""" - with self._shutdown_lock: - self._shutdown = True - # Drain all work items from the queue, and then cancel their - # associated futures. - while True: - try: - work_item = self._work_queue.get_nowait() - except queue.Empty: - break - if work_item is not None: - work_item.future.cancel() - # Send a wake-up to prevent threads calling - # _work_queue.get(block=True) from permanently blocking. - self._work_queue.put(None) # type: ignore[arg-type] - - # The above code is backported from python 3.9 - # - # For maintainability join_threads_or_timeout is - # a separate function since it is not a backport from - # cpython itself - # + def shutdown(self, *args: Any, **kwargs: Any) -> None: + """Shutdown with interrupt support added.""" + super().shutdown(wait=False, cancel_futures=True) self.join_threads_or_timeout() def join_threads_or_timeout(self) -> None: diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 9c98691c605ed..fdee7a7a90f29 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -30,7 +30,7 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: """ try: with open(filename, encoding="utf-8") as fdesc: - return json.loads(fdesc.read()) # type: ignore + return json.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error _LOGGER.debug("JSON file not found: %s", filename) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b967a6a0b1e82..4e76fa32de39f 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -115,7 +115,7 @@ def vincenty( cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda sigma = math.atan2(sinSigma, cosSigma) sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma - cosSqAlpha = 1 - sinAlpha ** 2 + cosSqAlpha = 1 - sinAlpha**2 try: cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha except ZeroDivisionError: @@ -124,14 +124,14 @@ def vincenty( LambdaPrev = Lambda Lambda = L + (1 - C) * FLATTENING * sinAlpha * ( sigma - + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)) + + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM**2)) ) if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD: break # successful convergence else: return None # failure to converge - uSq = cosSqAlpha * (AXIS_A ** 2 - AXIS_B ** 2) / (AXIS_B ** 2) + uSq = cosSqAlpha * (AXIS_A**2 - AXIS_B**2) / (AXIS_B**2) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) deltaSigma = ( @@ -142,12 +142,12 @@ def vincenty( + B / 4 * ( - cosSigma * (-1 + 2 * cos2SigmaM ** 2) + cosSigma * (-1 + 2 * cos2SigmaM**2) - B / 6 * cos2SigmaM - * (-3 + 4 * sinSigma ** 2) - * (-3 + 4 * cos2SigmaM ** 2) + * (-3 + 4 * sinSigma**2) + * (-3 + 4 * cos2SigmaM**2) ) ) ) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 9216993eb5359..d09feec52379b 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -33,6 +33,15 @@ def filter(self, record: logging.LogRecord) -> bool: class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Process the log in another thread.""" + def prepare(self, record: logging.LogRecord) -> logging.LogRecord: + """Prepare a record for queuing. + + This is added as a workaround for https://bugs.python.org/issue46755 + """ + record = super().prepare(record) + record.stack_info = None + return record + def handle(self, record: logging.LogRecord) -> Any: """ Conditionally emit the specified logging record. @@ -60,11 +69,11 @@ def async_activate_log_queue_handler(hass: HomeAssistant) -> None: This allows us to avoid blocking I/O and formatting messages in the event loop as log messages are written in another thread. """ - simple_queue = queue.SimpleQueue() # type: ignore + simple_queue: queue.SimpleQueue[logging.Handler] = queue.SimpleQueue() queue_handler = HomeAssistantQueueHandler(simple_queue) logging.root.addHandler(queue_handler) - migrated_handlers = [] + migrated_handlers: list[logging.Handler] = [] for handler in logging.root.handlers[:]: if handler is queue_handler: continue diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index e714b6b6b3152..396ab56b8c25d 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -59,6 +59,26 @@ def is_ip_address(address: str) -> bool: return True +def is_ipv4_address(address: str) -> bool: + """Check if a given string is an IPv4 address.""" + try: + IPv4Address(address) + except ValueError: + return False + + return True + + +def is_ipv6_address(address: str) -> bool: + """Check if a given string is an IPv6 address.""" + try: + IPv6Address(address) + except ValueError: + return False + + return True + + def normalize_url(address: str) -> str: """Normalize a given URL.""" url = yarl.URL(address.rstrip("/")) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index a1ee2b9f58453..aad93e375424f 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -50,7 +50,7 @@ def is_installed(package: str) -> bool: # was aborted while in progress see # https://github.com/home-assistant/core/issues/47699 if installed_version is None: - _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore + _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore[unreachable] return False return installed_version in req except PackageNotFoundError: diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py new file mode 100644 index 0000000000000..f9cc949afdce3 --- /dev/null +++ b/homeassistant/util/read_only_dict.py @@ -0,0 +1,23 @@ +"""Read only dictionary.""" +from typing import Any, TypeVar + + +def _readonly(*args: Any, **kwargs: Any) -> Any: + """Raise an exception when a read only dict is modified.""" + raise RuntimeError("Cannot modify ReadOnlyDict") + + +Key = TypeVar("Key") +Value = TypeVar("Value") + + +class ReadOnlyDict(dict[Key, Value]): + """Read only version of dict that is compatible with dict types.""" + + __setitem__ = _readonly + __delitem__ = _readonly + pop = _readonly + popitem = _readonly + clear = _readonly + update = _readonly + setdefault = _readonly diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index dfe73b0e937ae..e964fee798c89 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -134,7 +134,7 @@ def length(self, length: float | None, from_unit: str) -> float: raise TypeError(f"{length!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return distance_util.convert( # type: ignore + return distance_util.convert( # type: ignore[unreachable] length, from_unit, self.length_unit ) @@ -144,7 +144,7 @@ def accumulated_precipitation(self, precip: float | None, from_unit: str) -> flo raise TypeError(f"{precip!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return distance_util.convert( # type: ignore + return distance_util.convert( # type: ignore[unreachable] precip, from_unit, self.accumulated_precipitation_unit ) @@ -154,7 +154,7 @@ def pressure(self, pressure: float | None, from_unit: str) -> float: raise TypeError(f"{pressure!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return pressure_util.convert( # type: ignore + return pressure_util.convert( # type: ignore[unreachable] pressure, from_unit, self.pressure_unit ) @@ -164,7 +164,7 @@ def wind_speed(self, wind_speed: float | None, from_unit: str) -> float: raise TypeError(f"{wind_speed!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return speed_util.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore + return speed_util.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore[unreachable] def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" @@ -172,7 +172,7 @@ def volume(self, volume: float | None, from_unit: str) -> float: raise TypeError(f"{volume!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore + return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore[unreachable] def as_dict(self) -> dict[str, str]: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 8e9cb382b6c36..3eafc8abdd7cb 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -24,7 +24,7 @@ def save_yaml(path: str, data: dict) -> None: # From: https://gist.github.com/miracle2k/3184458 -def represent_odict( # type: ignore +def represent_odict( # type: ignore[no-untyped-def] dumper, tag, mapping, flow_style=None ) -> yaml.MappingNode: """Like BaseRepresenter.represent_mapping but does not issue the sort().""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index e6ac5fd364a8e..84349ef91a93d 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -100,7 +100,7 @@ def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line node: yaml.nodes.Node = super().compose_node(parent, index) # type: ignore[assignment] - node.__line__ = last_line + 1 # type: ignore + node.__line__ = last_line + 1 # type: ignore[attr-defined] return node @@ -149,7 +149,7 @@ def _add_reference( ... -def _add_reference(obj, loader: SafeLineLoader, node: yaml.nodes.Node): # type: ignore +def _add_reference(obj, loader: SafeLineLoader, node: yaml.nodes.Node): # type: ignore[no-untyped-def] """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) diff --git a/machine/raspberrypi b/machine/raspberrypi index 3f000b14db7af..960e343792d24 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi camera binaries. diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index 484b209b6faa6..225c45423a1ef 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi binaries. diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 1aec7ebf39f84..6315cc3e885ce 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi binaries. diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 165dc2e5397b6..51f41d6832066 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi binaries. diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 1aec7ebf39f84..6315cc3e885ce 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi binaries. diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 165dc2e5397b6..51f41d6832066 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -7,7 +7,8 @@ RUN apk --no-cache add \ usbutils \ && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt + RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver ## # Set symlinks for raspberry pi binaries. diff --git a/machine/tinker b/machine/tinker index 04a0aa6dc2caa..9660ca71b9cbf 100644 --- a/machine/tinker +++ b/machine/tinker @@ -4,6 +4,7 @@ FROM homeassistant/armv7-homeassistant:$BUILD_VERSION RUN apk --no-cache add usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ + --use-deprecated=legacy-resolver \ bluepy \ pybluez \ pygatt[GATTTOOL] diff --git a/mypy.ini b/mypy.ini index 886b0fce2ceda..781bc5c199e1f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ # Automatically generated by hassfest. # -# To update, run python3 -m script.hassfest +# To update, run python3 -m script.hassfest -p mypy_config [mypy] python_version = 3.9 @@ -70,9 +70,15 @@ disallow_any_generics = true [mypy-homeassistant.helpers.translation] disallow_any_generics = true +[mypy-homeassistant.util.async_] +disallow_any_generics = true + [mypy-homeassistant.util.color] disallow_any_generics = true +[mypy-homeassistant.util.decorator] +disallow_any_generics = true + [mypy-homeassistant.util.process] disallow_any_generics = true @@ -444,6 +450,61 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.deconz] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.deconz.config_flow] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.deconz.diagnostics] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.deconz.gateway] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.deconz.services] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.device_automation.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -774,6 +835,94 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit_controller] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.alarm_control_panel] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.button] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.const] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.lock] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.select] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.storage] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.homekit_controller.utils] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homewizard.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -862,6 +1011,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.isy994.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.iqvia.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1016,6 +1176,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mjpeg.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.modbus.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1269,6 +1440,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.powerwall.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.proximity.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1291,6 +1473,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.pure_energie.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rainmachine.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1401,6 +1594,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.roku.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rpi_power.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1511,7 +1715,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.smhi.*] +[mypy-homeassistant.components.sleepiq.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1522,7 +1726,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.sonos.media_player] +[mypy-homeassistant.components.smhi.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -1973,6 +2177,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wiz.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2031,191 +2246,644 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.blueprint.*] +[mypy-homeassistant.components.blueprint.importer] ignore_errors = true -[mypy-homeassistant.components.cloud.*] +[mypy-homeassistant.components.blueprint.models] ignore_errors = true -[mypy-homeassistant.components.config.*] +[mypy-homeassistant.components.blueprint.websocket_api] ignore_errors = true -[mypy-homeassistant.components.conversation.*] +[mypy-homeassistant.components.cloud.client] ignore_errors = true -[mypy-homeassistant.components.deconz.*] +[mypy-homeassistant.components.cloud.http_api] ignore_errors = true -[mypy-homeassistant.components.demo.*] +[mypy-homeassistant.components.conversation] ignore_errors = true -[mypy-homeassistant.components.denonavr.*] +[mypy-homeassistant.components.conversation.default_agent] ignore_errors = true -[mypy-homeassistant.components.evohome.*] +[mypy-homeassistant.components.deconz.alarm_control_panel] ignore_errors = true -[mypy-homeassistant.components.fireservicerota.*] +[mypy-homeassistant.components.deconz.binary_sensor] ignore_errors = true -[mypy-homeassistant.components.firmata.*] +[mypy-homeassistant.components.deconz.climate] ignore_errors = true -[mypy-homeassistant.components.freebox.*] +[mypy-homeassistant.components.deconz.cover] ignore_errors = true -[mypy-homeassistant.components.geniushub.*] +[mypy-homeassistant.components.deconz.fan] ignore_errors = true -[mypy-homeassistant.components.google_assistant.*] +[mypy-homeassistant.components.deconz.light] ignore_errors = true -[mypy-homeassistant.components.gree.*] +[mypy-homeassistant.components.deconz.lock] ignore_errors = true -[mypy-homeassistant.components.harmony.*] +[mypy-homeassistant.components.deconz.logbook] ignore_errors = true -[mypy-homeassistant.components.hassio.*] +[mypy-homeassistant.components.deconz.number] ignore_errors = true -[mypy-homeassistant.components.here_travel_time.*] +[mypy-homeassistant.components.deconz.sensor] ignore_errors = true -[mypy-homeassistant.components.home_plus_control.*] +[mypy-homeassistant.components.deconz.siren] ignore_errors = true -[mypy-homeassistant.components.homekit.*] +[mypy-homeassistant.components.deconz.switch] ignore_errors = true -[mypy-homeassistant.components.homekit_controller.*] +[mypy-homeassistant.components.denonavr.config_flow] ignore_errors = true -[mypy-homeassistant.components.honeywell.*] +[mypy-homeassistant.components.denonavr.media_player] ignore_errors = true -[mypy-homeassistant.components.icloud.*] +[mypy-homeassistant.components.denonavr.receiver] ignore_errors = true -[mypy-homeassistant.components.influxdb.*] +[mypy-homeassistant.components.evohome] ignore_errors = true -[mypy-homeassistant.components.input_datetime.*] +[mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.isy994.*] +[mypy-homeassistant.components.evohome.water_heater] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.helpers] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.http] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.report_state] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.trait] +ignore_errors = true + +[mypy-homeassistant.components.gree.climate] +ignore_errors = true + +[mypy-homeassistant.components.gree.switch] +ignore_errors = true + +[mypy-homeassistant.components.harmony] +ignore_errors = true + +[mypy-homeassistant.components.harmony.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.harmony.data] +ignore_errors = true + +[mypy-homeassistant.components.hassio] +ignore_errors = true + +[mypy-homeassistant.components.hassio.auth] +ignore_errors = true + +[mypy-homeassistant.components.hassio.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.hassio.ingress] +ignore_errors = true + +[mypy-homeassistant.components.hassio.sensor] +ignore_errors = true + +[mypy-homeassistant.components.hassio.system_health] +ignore_errors = true + +[mypy-homeassistant.components.hassio.websocket_api] +ignore_errors = true + +[mypy-homeassistant.components.here_travel_time.sensor] +ignore_errors = true + +[mypy-homeassistant.components.home_plus_control] +ignore_errors = true + +[mypy-homeassistant.components.home_plus_control.api] +ignore_errors = true + +[mypy-homeassistant.components.homekit.aidmanager] +ignore_errors = true + +[mypy-homeassistant.components.homekit.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.homekit.util] +ignore_errors = true + +[mypy-homeassistant.components.honeywell.climate] +ignore_errors = true + +[mypy-homeassistant.components.icloud] +ignore_errors = true + +[mypy-homeassistant.components.icloud.account] +ignore_errors = true + +[mypy-homeassistant.components.icloud.device_tracker] +ignore_errors = true + +[mypy-homeassistant.components.icloud.sensor] +ignore_errors = true + +[mypy-homeassistant.components.influxdb] +ignore_errors = true + +[mypy-homeassistant.components.input_datetime] +ignore_errors = true + +[mypy-homeassistant.components.izone.climate] +ignore_errors = true + +[mypy-homeassistant.components.konnected] +ignore_errors = true + +[mypy-homeassistant.components.konnected.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.helper] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.select] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.sensor] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.switch] +ignore_errors = true + +[mypy-homeassistant.components.lovelace] +ignore_errors = true + +[mypy-homeassistant.components.lovelace.dashboard] +ignore_errors = true + +[mypy-homeassistant.components.lovelace.resources] +ignore_errors = true + +[mypy-homeassistant.components.lovelace.websocket] +ignore_errors = true + +[mypy-homeassistant.components.lutron_caseta] +ignore_errors = true + +[mypy-homeassistant.components.lutron_caseta.device_trigger] +ignore_errors = true + +[mypy-homeassistant.components.lutron_caseta.switch] +ignore_errors = true + +[mypy-homeassistant.components.lyric.climate] +ignore_errors = true + +[mypy-homeassistant.components.lyric.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.lyric.sensor] +ignore_errors = true + +[mypy-homeassistant.components.melcloud] +ignore_errors = true + +[mypy-homeassistant.components.melcloud.climate] +ignore_errors = true + +[mypy-homeassistant.components.meteo_france.sensor] +ignore_errors = true + +[mypy-homeassistant.components.meteo_france.weather] +ignore_errors = true + +[mypy-homeassistant.components.minecraft_server] +ignore_errors = true + +[mypy-homeassistant.components.minecraft_server.helpers] +ignore_errors = true + +[mypy-homeassistant.components.minecraft_server.sensor] +ignore_errors = true + +[mypy-homeassistant.components.netgear] +ignore_errors = true + +[mypy-homeassistant.components.netgear.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.netgear.device_tracker] +ignore_errors = true + +[mypy-homeassistant.components.netgear.router] +ignore_errors = true + +[mypy-homeassistant.components.nilu.air_quality] +ignore_errors = true + +[mypy-homeassistant.components.nzbget] +ignore_errors = true + +[mypy-homeassistant.components.nzbget.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.nzbget.coordinator] +ignore_errors = true + +[mypy-homeassistant.components.nzbget.switch] +ignore_errors = true + +[mypy-homeassistant.components.omnilogic.common] +ignore_errors = true + +[mypy-homeassistant.components.omnilogic.sensor] +ignore_errors = true + +[mypy-homeassistant.components.omnilogic.switch] +ignore_errors = true + +[mypy-homeassistant.components.onvif.base] +ignore_errors = true + +[mypy-homeassistant.components.onvif.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.onvif.button] +ignore_errors = true + +[mypy-homeassistant.components.onvif.camera] +ignore_errors = true + +[mypy-homeassistant.components.onvif.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.onvif.device] +ignore_errors = true + +[mypy-homeassistant.components.onvif.event] +ignore_errors = true + +[mypy-homeassistant.components.onvif.models] +ignore_errors = true + +[mypy-homeassistant.components.onvif.parsers] +ignore_errors = true + +[mypy-homeassistant.components.onvif.sensor] +ignore_errors = true + +[mypy-homeassistant.components.ozw] +ignore_errors = true + +[mypy-homeassistant.components.ozw.climate] +ignore_errors = true + +[mypy-homeassistant.components.ozw.entity] +ignore_errors = true + +[mypy-homeassistant.components.philips_js] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.device_trigger] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.light] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.media_player] +ignore_errors = true + +[mypy-homeassistant.components.plex.media_player] +ignore_errors = true + +[mypy-homeassistant.components.profiler] +ignore_errors = true + +[mypy-homeassistant.components.solaredge.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.solaredge.coordinator] +ignore_errors = true + +[mypy-homeassistant.components.solaredge.sensor] +ignore_errors = true + +[mypy-homeassistant.components.sonos] +ignore_errors = true + +[mypy-homeassistant.components.sonos.alarms] +ignore_errors = true + +[mypy-homeassistant.components.sonos.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.sonos.diagnostics] +ignore_errors = true + +[mypy-homeassistant.components.sonos.entity] +ignore_errors = true + +[mypy-homeassistant.components.sonos.favorites] +ignore_errors = true + +[mypy-homeassistant.components.sonos.helpers] +ignore_errors = true + +[mypy-homeassistant.components.sonos.media_browser] +ignore_errors = true + +[mypy-homeassistant.components.sonos.media_player] +ignore_errors = true + +[mypy-homeassistant.components.sonos.number] +ignore_errors = true + +[mypy-homeassistant.components.sonos.sensor] +ignore_errors = true + +[mypy-homeassistant.components.sonos.speaker] +ignore_errors = true + +[mypy-homeassistant.components.sonos.statistics] +ignore_errors = true + +[mypy-homeassistant.components.system_health] +ignore_errors = true + +[mypy-homeassistant.components.telegram_bot.polling] +ignore_errors = true + +[mypy-homeassistant.components.template.number] +ignore_errors = true + +[mypy-homeassistant.components.template.sensor] +ignore_errors = true + +[mypy-homeassistant.components.toon] +ignore_errors = true + +[mypy-homeassistant.components.toon.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.toon.models] +ignore_errors = true + +[mypy-homeassistant.components.unifi] +ignore_errors = true + +[mypy-homeassistant.components.unifi.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.unifi.device_tracker] +ignore_errors = true + +[mypy-homeassistant.components.unifi.diagnostics] +ignore_errors = true + +[mypy-homeassistant.components.unifi.unifi_entity_base] +ignore_errors = true + +[mypy-homeassistant.components.upnp] +ignore_errors = true + +[mypy-homeassistant.components.upnp.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.upnp.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.upnp.device] +ignore_errors = true + +[mypy-homeassistant.components.upnp.sensor] +ignore_errors = true + +[mypy-homeassistant.components.vizio.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.vizio.media_player] +ignore_errors = true + +[mypy-homeassistant.components.withings] +ignore_errors = true + +[mypy-homeassistant.components.withings.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.withings.common] +ignore_errors = true + +[mypy-homeassistant.components.withings.config_flow] +ignore_errors = true + +[mypy-homeassistant.components.xbox] +ignore_errors = true + +[mypy-homeassistant.components.xbox.base_sensor] +ignore_errors = true + +[mypy-homeassistant.components.xbox.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.xbox.browse_media] +ignore_errors = true + +[mypy-homeassistant.components.xbox.media_source] +ignore_errors = true + +[mypy-homeassistant.components.xbox.sensor] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara.lock] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara.sensor] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.air_quality] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.device] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.device_tracker] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.fan] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.humidifier] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.light] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.sensor] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.switch] +ignore_errors = true + +[mypy-homeassistant.components.yeelight] +ignore_errors = true + +[mypy-homeassistant.components.yeelight.light] +ignore_errors = true + +[mypy-homeassistant.components.yeelight.scanner] +ignore_errors = true + +[mypy-homeassistant.components.zha.alarm_control_panel] +ignore_errors = true + +[mypy-homeassistant.components.zha.api] +ignore_errors = true + +[mypy-homeassistant.components.zha.binary_sensor] +ignore_errors = true + +[mypy-homeassistant.components.zha.button] ignore_errors = true -[mypy-homeassistant.components.izone.*] +[mypy-homeassistant.components.zha.climate] ignore_errors = true -[mypy-homeassistant.components.konnected.*] +[mypy-homeassistant.components.zha.config_flow] ignore_errors = true -[mypy-homeassistant.components.kostal_plenticore.*] +[mypy-homeassistant.components.zha.core.channels] ignore_errors = true -[mypy-homeassistant.components.litterrobot.*] +[mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true -[mypy-homeassistant.components.lovelace.*] +[mypy-homeassistant.components.zha.core.channels.closures] ignore_errors = true -[mypy-homeassistant.components.lutron_caseta.*] +[mypy-homeassistant.components.zha.core.channels.general] ignore_errors = true -[mypy-homeassistant.components.lyric.*] +[mypy-homeassistant.components.zha.core.channels.homeautomation] ignore_errors = true -[mypy-homeassistant.components.melcloud.*] +[mypy-homeassistant.components.zha.core.channels.hvac] ignore_errors = true -[mypy-homeassistant.components.meteo_france.*] +[mypy-homeassistant.components.zha.core.channels.lighting] ignore_errors = true -[mypy-homeassistant.components.minecraft_server.*] +[mypy-homeassistant.components.zha.core.channels.lightlink] ignore_errors = true -[mypy-homeassistant.components.mobile_app.*] +[mypy-homeassistant.components.zha.core.channels.manufacturerspecific] ignore_errors = true -[mypy-homeassistant.components.nest.legacy.*] +[mypy-homeassistant.components.zha.core.channels.measurement] ignore_errors = true -[mypy-homeassistant.components.netgear.*] +[mypy-homeassistant.components.zha.core.channels.protocol] ignore_errors = true -[mypy-homeassistant.components.nilu.*] +[mypy-homeassistant.components.zha.core.channels.security] ignore_errors = true -[mypy-homeassistant.components.nzbget.*] +[mypy-homeassistant.components.zha.core.channels.smartenergy] ignore_errors = true -[mypy-homeassistant.components.omnilogic.*] +[mypy-homeassistant.components.zha.core.decorators] ignore_errors = true -[mypy-homeassistant.components.onvif.*] +[mypy-homeassistant.components.zha.core.device] ignore_errors = true -[mypy-homeassistant.components.ozw.*] +[mypy-homeassistant.components.zha.core.discovery] ignore_errors = true -[mypy-homeassistant.components.philips_js.*] +[mypy-homeassistant.components.zha.core.gateway] ignore_errors = true -[mypy-homeassistant.components.plex.*] +[mypy-homeassistant.components.zha.core.group] ignore_errors = true -[mypy-homeassistant.components.profiler.*] +[mypy-homeassistant.components.zha.core.helpers] ignore_errors = true -[mypy-homeassistant.components.solaredge.*] +[mypy-homeassistant.components.zha.core.registries] ignore_errors = true -[mypy-homeassistant.components.sonos.*] +[mypy-homeassistant.components.zha.core.store] ignore_errors = true -[mypy-homeassistant.components.spotify.*] +[mypy-homeassistant.components.zha.core.typing] ignore_errors = true -[mypy-homeassistant.components.system_health.*] +[mypy-homeassistant.components.zha.cover] ignore_errors = true -[mypy-homeassistant.components.telegram_bot.*] +[mypy-homeassistant.components.zha.device_action] ignore_errors = true -[mypy-homeassistant.components.template.*] +[mypy-homeassistant.components.zha.device_tracker] ignore_errors = true -[mypy-homeassistant.components.toon.*] +[mypy-homeassistant.components.zha.entity] ignore_errors = true -[mypy-homeassistant.components.unifi.*] +[mypy-homeassistant.components.zha.fan] ignore_errors = true -[mypy-homeassistant.components.upnp.*] +[mypy-homeassistant.components.zha.light] ignore_errors = true -[mypy-homeassistant.components.vizio.*] +[mypy-homeassistant.components.zha.lock] ignore_errors = true -[mypy-homeassistant.components.withings.*] +[mypy-homeassistant.components.zha.select] ignore_errors = true -[mypy-homeassistant.components.xbox.*] +[mypy-homeassistant.components.zha.sensor] ignore_errors = true -[mypy-homeassistant.components.xiaomi_aqara.*] +[mypy-homeassistant.components.zha.siren] ignore_errors = true -[mypy-homeassistant.components.xiaomi_miio.*] +[mypy-homeassistant.components.zha.switch] ignore_errors = true -[mypy-homeassistant.components.yeelight.*] +[mypy-homeassistant.components.zwave] ignore_errors = true -[mypy-homeassistant.components.zha.*] +[mypy-homeassistant.components.zwave.migration] ignore_errors = true -[mypy-homeassistant.components.zwave.*] +[mypy-homeassistant.components.zwave.node_entity] ignore_errors = true diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 288fcc560c385..cb90499b6ca9a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -11,6 +11,8 @@ from homeassistant.const import Platform +UNDEFINED = object() + @dataclass class TypeHintMatch: @@ -39,9 +41,11 @@ class TypeHintMatch: f"^homeassistant\\.components\\.\\w+\\.({'|'.join([platform.value for platform in Platform])})$" ), # device_tracker matches only in the package root (device_tracker.py) - "device_tracker": re.compile( - f"^homeassistant\\.components\\.\\w+\\.({Platform.DEVICE_TRACKER.value})$" - ), + "device_tracker": re.compile(r"^homeassistant\.components\.\w+\.(device_tracker)$"), + # diagnostics matches only in the package root (diagnostics.py) + "diagnostics": re.compile(r"^homeassistant\.components\.\w+\.(diagnostics)$"), + # config_flow matches only in the package root (config_flow.py) + "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$") } _METHOD_MATCH: list[TypeHintMatch] = [ @@ -171,11 +175,41 @@ class TypeHintMatch: }, return_type=["DeviceScanner", "DeviceScanner | None"], ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["diagnostics"], + function_name="async_get_config_entry_diagnostics", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type=UNDEFINED, + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["diagnostics"], + function_name="async_get_device_diagnostics", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + 2: "DeviceEntry", + }, + return_type=UNDEFINED, + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["config_flow"], + function_name="_async_has_devices", + arg_types={ + 0: "HomeAssistant", + }, + return_type="bool", + ), ] def _is_valid_type(expected_type: list[str] | str | None, node: astroid.NodeNG) -> bool: """Check the argument node against the expected type.""" + if expected_type is UNDEFINED: + return True + if isinstance(expected_type, list): for expected_type_item in expected_type: if _is_valid_type(expected_type_item, node): diff --git a/pyproject.toml b/pyproject.toml index 69398645d1830..c1a08a3f0c3c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools~=60.5", "wheel~=0.37.1"] build-backend = "setuptools.build_meta" [tool.black] -target-version = ["py38"] +target-version = ["py39", "py310"] exclude = 'generated' [tool.isort] @@ -28,7 +28,16 @@ ignore = [ # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs = 2 -init-hook='from pylint.config.find_default_config_files import find_default_config_files; from pathlib import Path; import sys; sys.path.append(str(Path(Path(list(find_default_config_files())[0]).parent, "pylint/plugins")))' +init-hook = """\ + from pathlib import Path; \ + import sys; \ + + from pylint.config import find_default_config_files; \ + + sys.path.append( \ + str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) + ) \ + """ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", diff --git a/requirements.txt b/requirements.txt index c8ee1d91368ac..4885e717b30fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==22.1.0 +awesomeversion==22.2.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 @@ -15,7 +15,7 @@ ifaddr==0.1.7 jinja2==3.0.3 PyJWT==2.1.0 cryptography==35.0.0 -pip>=8.0.3,<20.3 +pip>=21.0,<22.1 python-slugify==4.0.1 pyyaml==6.0 requests==2.27.1 diff --git a/requirements_all.txt b/requirements_all.txt index 739bb68f0a3dc..9714786a873a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.2 +# PySwitchbot==0.13.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -114,7 +114,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.0 +advantage_air==0.3.1 # homeassistant.components.frontier_silicon afsapi==0.0.4 @@ -150,7 +150,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.7 +aiodiscover==1.4.8 # homeassistant.components.dnsip # homeassistant.components.minecraft_server @@ -175,7 +175,7 @@ aioflo==2021.11.0 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==22.2.0 +aiogithubapi==22.2.3 # homeassistant.components.guardian aioguardian==2021.11.0 @@ -184,14 +184,14 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.6.11 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 @@ -224,7 +224,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.3 # homeassistant.components.nanoleaf -aionanoleaf==0.1.1 +aionanoleaf==0.2.0 # homeassistant.components.keyboard_remote aionotify==0.2.0 @@ -244,6 +244,9 @@ aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 +# homeassistant.components.sonarr +aiopyarr==22.2.2 + # homeassistant.components.recollect_waste aiorecollect==1.0.8 @@ -254,7 +257,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.9 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 @@ -269,7 +272,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.2 # homeassistant.components.unifi -aiounifi==30 +aiounifi==31 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -278,7 +281,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.2 +aiowebostv==0.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -322,11 +325,8 @@ anthemav==1.2.0 # homeassistant.components.apcupsd apcaccess==0.0.13 -# homeassistant.components.apns -apns2==0.3.0 - # homeassistant.components.apprise -apprise==0.9.6 +apprise==0.9.7 # homeassistant.components.aprs aprslib==0.7.0 @@ -347,14 +347,18 @@ asmog==0.0.6 asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr +# homeassistant.components.dlna_dms # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.4 +async-upnp-client==0.23.5 # homeassistant.components.supla asyncpysupla==0.0.5 +# homeassistant.components.sleepiq +asyncsleepiq==1.1.0 + # homeassistant.components.aten_pe atenpdu==0.3.2 @@ -377,7 +381,7 @@ av==8.1.0 axis==44 # homeassistant.components.azure_event_hub -azure-eventhub==5.5.0 +azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus azure-servicebus==0.50.3 @@ -404,7 +408,7 @@ beautifulsoup4==4.10.0 bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.10 +bimmer_connected==0.8.11 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -457,7 +461,7 @@ brother==1.1.0 brottsplatskartan==0.0.1 # homeassistant.components.brunt -brunt==1.1.1 +brunt==1.2.0 # homeassistant.components.bsblan bsblan==0.5.0 @@ -565,9 +569,6 @@ directv==0.4.0 # homeassistant.components.discogs discogs_client==2.3.0 -# homeassistant.components.discord -discord.py==1.7.3 - # homeassistant.components.steamist discovery30303==0.2.1 @@ -605,14 +606,11 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==1.0.0 +elkm1-lib==1.2.0 # homeassistant.components.elmax elmax_api==0.0.2 -# homeassistant.components.mobile_app -emoji==1.6.3 - # homeassistant.components.emulated_roku emulated_roku==0.2.1 @@ -671,6 +669,9 @@ fints==1.0.1 # homeassistant.components.fitbit fitbit==0.3.1 +# homeassistant.components.fivem +fivem-api==0.1.2 + # homeassistant.components.fixer fixerio==1.0.0a0 @@ -678,10 +679,10 @@ fixerio==1.0.0a0 fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.26 +flux_led==0.28.27 # homeassistant.components.homekit fnvhash==0.1.0 @@ -745,9 +746,6 @@ gitterpy==0.1.7 # homeassistant.components.glances glances_api==0.3.4 -# homeassistant.components.gntp -gntp==1.0.3 - # homeassistant.components.goalzero goalzero==0.2.1 @@ -782,11 +780,14 @@ gps3==0.33.3 greeclimate==1.0.2 # homeassistant.components.greeneye_monitor -greeneye_monitor==3.0.1 +greeneye_monitor==3.0.3 # homeassistant.components.greenwave greenwavereality==0.5.1 +# homeassistant.components.pure_energie +gridnet==4.0.0 + # homeassistant.components.growatt_server growattServer==1.1.0 @@ -809,7 +810,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.52.0 +hass-nabucasa==0.54.0 # homeassistant.components.splunk hass_splunk==0.1.1 @@ -839,13 +840,13 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.12 +holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220203.1 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.10 +# homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect homeconnect==0.6.3 @@ -914,7 +915,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.6 +intellifire4py==0.9.9 # homeassistant.components.iotawatt iotawattpy==0.1.0 @@ -1048,8 +1049,11 @@ minio==5.0.10 # homeassistant.components.mitemp_bt mitemp_bt==0.0.5 +# homeassistant.components.moehlenhoff_alpha2 +moehlenhoff-alpha2==1.1.2 + # homeassistant.components.motion_blinds -motionblinds==0.5.12 +motionblinds==0.5.13 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -1096,6 +1100,9 @@ nexia==0.9.13 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 +# homeassistant.components.discord +nextcord==2.0.0a8 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 @@ -1191,7 +1198,7 @@ oru==0.1.11 orvibo==1.1.1 # homeassistant.components.ovo_energy -ovoenergy==1.1.12 +ovoenergy==1.2.0 # homeassistant.components.p1_monitor p1monitor==1.0.1 @@ -1255,7 +1262,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 @@ -1264,7 +1271,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.8.5 +plugwise==0.16.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1313,7 +1320,7 @@ pushover_complete==1.1.1 pvo==0.2.2 # homeassistant.components.rpi_gpio_pwm -pwmled==1.6.7 +pwmled==1.6.10 # homeassistant.components.canary py-canary==0.5.1 @@ -1359,7 +1366,7 @@ pyRFXtrx==0.27.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.21.7 +pyTibber==0.22.1 # homeassistant.components.dlink pyW215==0.7.0 @@ -1404,7 +1411,7 @@ pyatome==0.1.1 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.9 +pyaussiebb==0.0.11 # homeassistant.components.balboa pybalboa==0.13 @@ -1464,13 +1471,13 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==86 +pydeconz==87 # homeassistant.components.delijn -pydelijn==0.6.1 +pydelijn==1.0.0 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -1485,13 +1492,13 @@ pydroid-ipcam==0.8 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.14 +pyeconet==0.1.15 # homeassistant.components.edimax pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.1.5 +pyefergy==22.1.1 # homeassistant.components.eight_sleep pyeight==0.2.0 @@ -1539,7 +1546,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.2 +pyfritzhome==0.6.4 # homeassistant.components.fronius pyfronius==0.7.1 @@ -1558,7 +1565,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.11.1 +pyhaversion==22.02.0 # homeassistant.components.heos pyheos==0.7.2 @@ -1579,7 +1586,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.icloud -pyicloud==0.10.2 +pyicloud==1.0.0 # homeassistant.components.insteon pyinsteon==1.0.16 @@ -1702,7 +1709,7 @@ pynetgear==0.9.1 pynetio==0.1.9.1 # homeassistant.components.nina -pynina==0.1.4 +pynina==0.1.7 # homeassistant.components.nuki pynuki==1.5.2 @@ -1746,7 +1753,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.2 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1758,7 +1765,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.13 +pypck==0.7.14 # homeassistant.components.pjlink pypjlink2==1.2.1 @@ -1815,7 +1822,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.3 +pysensibo==1.0.7 # homeassistant.components.serial # homeassistant.components.zha @@ -1945,7 +1952,7 @@ python-kasa==0.4.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.2 +python-miio==0.5.10 # homeassistant.components.mpd python-mpd2==3.0.4 @@ -1975,7 +1982,7 @@ python-smarttub==0.0.29 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.12 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 @@ -1983,9 +1990,6 @@ python-tado==0.12.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.vlc python-vlc==1.1.2 @@ -2008,7 +2012,7 @@ pytouchline==0.7 pytraccar==0.10.0 # homeassistant.components.tradfri -pytradfri[async]==8.0.1 +pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation @@ -2021,7 +2025,7 @@ pyudev==0.22.0 pyunifiprotect==3.2.0 # homeassistant.components.uptimerobot -pyuptimerobot==21.11.0 +pyuptimerobot==22.2.0 # homeassistant.components.keyboard # pyuserinput==0.1.11 @@ -2053,6 +2057,9 @@ pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 +# homeassistant.components.wiz +pywizlight==0.5.13 + # homeassistant.components.xeoma pyxeoma==1.4.1 @@ -2071,6 +2078,9 @@ quantum-gateway==0.0.6 # homeassistant.components.rachio rachiopy==1.0.3 +# homeassistant.components.radio_browser +radios==0.1.0 + # homeassistant.components.radiotherm radiotherm==2.1.0 @@ -2087,7 +2097,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.8 +renault-api==0.1.9 # homeassistant.components.python_script restrictedpython==5.2 @@ -2111,7 +2121,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.12.0 +rokuecp==0.14.1 # homeassistant.components.roomba roombapy==1.6.5 @@ -2144,7 +2154,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.6.0 +samsungtvws==1.7.0 # homeassistant.components.satel_integra satel_integra==0.3.4 @@ -2169,10 +2179,10 @@ sense-hat==2.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.6 +sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.3 +sentry-sdk==1.5.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -2201,9 +2211,6 @@ skybellpy==0.6.3 # homeassistant.components.slack slackclient==2.5.0 -# homeassistant.components.sleepiq -sleepyq==0.8.1 - # homeassistant.components.xmpp slixmpp==1.7.1 @@ -2228,7 +2235,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.2 +soco==0.26.3 # homeassistant.components.solaredge_local solaredge-local==0.2.0 @@ -2245,9 +2252,6 @@ somecomfort==0.8.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 -# homeassistant.components.sonarr -sonarr==0.3.0 - # homeassistant.components.marytts speak2mary==1.4.0 @@ -2266,7 +2270,7 @@ spotipy==2.19.0 sqlalchemy==1.4.27 # homeassistant.components.srp_energy -srpenergy==1.3.2 +srpenergy==1.3.6 # homeassistant.components.starline starline==0.1.5 @@ -2293,7 +2297,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.3.12 +subarulink==0.4.2 # homeassistant.components.ecovacs sucks==0.9.4 @@ -2311,7 +2315,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==2.2.3 +systembridge==2.3.1 # homeassistant.components.tailscale tailscale==0.2.0 @@ -2341,7 +2345,7 @@ temperusb==1.5.3 # tensorflow==2.5.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.12 +tesla-powerwall==0.3.17 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 @@ -2371,7 +2375,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.1 +total_connect_client==2022.2.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 @@ -2380,7 +2384,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 @@ -2391,6 +2395,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 @@ -2408,6 +2415,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru +# homeassistant.components.zwave_me url-normalize==1.4.1 # homeassistant.components.uscis @@ -2417,13 +2425,13 @@ uscisstatus==0.1.1 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.9.0 +vallox-websocket-api==2.11.0 # homeassistant.components.rdw vehicle==0.3.1 # homeassistant.components.velbus -velbus-aio==2022.2.1 +velbus-aio==2022.2.4 # homeassistant.components.venstar venstarcolortouch==0.15 @@ -2478,7 +2486,7 @@ wiffi==1.1.0 wirelesstagpy==0.8.1 # homeassistant.components.withings -withings-api==2.3.2 +withings-api==2.4.0 # homeassistant.components.wled wled==0.13.0 @@ -2486,9 +2494,6 @@ wled==0.13.0 # homeassistant.components.wolflink wolf_smartset==0.1.11 -# homeassistant.components.xbee -xbee-helper==0.0.7 - # homeassistant.components.xbox xbox-webapi==2.0.11 @@ -2510,13 +2515,13 @@ xmltodict==0.12.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.7 +yalesmartalarmclient==0.3.8 # homeassistant.components.august yalexs==1.1.22 # homeassistant.components.yeelight -yeelight==0.7.8 +yeelight==0.7.9 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 @@ -2531,10 +2536,10 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.66 +zha-quirks==0.0.67 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2549,7 +2554,7 @@ zigpy-deconz==0.14.0 zigpy-xbee==0.14.0 # homeassistant.components.zha -zigpy-zigate==0.7.3 +zigpy-zigate==0.8.0 # homeassistant.components.zha zigpy-znp==0.7.0 @@ -2561,4 +2566,7 @@ zigpy==0.43.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.34.0 +zwave-js-server-python==0.35.1 + +# homeassistant.components.zwave_me +zwave_me_ws==0.2.1 diff --git a/requirements_test.txt b/requirements_test.txt index 6c516c09d8356..bf98c8a449ec7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,9 +8,8 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.2.0 +coverage==6.3.1 freezegun==1.1.0 -jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.931 pre-commit==2.17.0 @@ -25,9 +24,8 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 pytest-xdist==2.4.0 -pytest==6.2.5 +pytest==7.0.1 requests_mock==1.9.2 -responses==0.12.0 respx==0.19.0 stdlib-list==0.7.0 tqdm==4.49.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3def0a7de1054..8f53952071330 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -27,7 +27,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -# PySwitchbot==0.13.2 +# PySwitchbot==0.13.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -70,7 +70,7 @@ adext==0.4.2 adguardhome==0.5.1 # homeassistant.components.advantage_air -advantage_air==0.3.0 +advantage_air==0.3.1 # homeassistant.components.agent_dvr agent-py==0.0.23 @@ -103,7 +103,7 @@ aioazuredevops==1.3.5 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.7 +aiodiscover==1.4.8 # homeassistant.components.dnsip # homeassistant.components.minecraft_server @@ -125,7 +125,7 @@ aioesphomeapi==10.8.2 aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.2.0 +aiogithubapi==22.2.3 # homeassistant.components.guardian aioguardian==2021.11.0 @@ -134,14 +134,14 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.6.11 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 @@ -162,7 +162,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.3 # homeassistant.components.nanoleaf -aionanoleaf==0.1.1 +aionanoleaf==0.2.0 # homeassistant.components.notion aionotion==3.0.2 @@ -179,6 +179,9 @@ aiopvapi==1.6.19 # homeassistant.components.pvpc_hourly_pricing aiopvpc==3.0.0 +# homeassistant.components.sonarr +aiopyarr==22.2.2 + # homeassistant.components.recollect_waste aiorecollect==1.0.8 @@ -189,7 +192,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.9 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 @@ -204,7 +207,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.2 # homeassistant.components.unifi -aiounifi==30 +aiounifi==31 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -213,7 +216,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.2 +aiowebostv==0.1.3 # homeassistant.components.yandex_transport aioymaps==1.2.2 @@ -239,11 +242,8 @@ ambiclimate==0.2.1 # homeassistant.components.androidtv androidtv[async]==0.0.63 -# homeassistant.components.apns -apns2==0.3.0 - # homeassistant.components.apprise -apprise==0.9.6 +apprise==0.9.7 # homeassistant.components.aprs aprslib==0.7.0 @@ -252,10 +252,14 @@ aprslib==0.7.0 arcam-fmj==0.12.0 # homeassistant.components.dlna_dmr +# homeassistant.components.dlna_dms # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.4 +async-upnp-client==0.23.5 + +# homeassistant.components.sleepiq +asyncsleepiq==1.1.0 # homeassistant.components.aurora auroranoaa==0.0.2 @@ -270,16 +274,19 @@ av==8.1.0 axis==44 # homeassistant.components.azure_event_hub -azure-eventhub==5.5.0 +azure-eventhub==5.7.0 # homeassistant.components.homekit base36==0.1.1 +# homeassistant.components.scrape +beautifulsoup4==4.10.0 + # homeassistant.components.zha bellows==0.29.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.10 +bimmer_connected==0.8.11 # homeassistant.components.blebox blebox_uniapi==1.3.3 @@ -303,7 +310,7 @@ broadlink==0.18.0 brother==1.1.0 # homeassistant.components.brunt -brunt==1.1.1 +brunt==1.2.0 # homeassistant.components.bsblan bsblan==0.5.0 @@ -388,14 +395,11 @@ dynalite_devices==0.1.46 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==1.0.0 +elkm1-lib==1.2.0 # homeassistant.components.elmax elmax_api==0.0.2 -# homeassistant.components.mobile_app -emoji==1.6.3 - # homeassistant.components.emulated_roku emulated_roku==0.2.1 @@ -420,14 +424,17 @@ faadelays==0.0.7 # homeassistant.components.feedreader feedparser==6.0.2 +# homeassistant.components.fivem +fivem-api==0.1.2 + # homeassistant.components.fjaraskupan fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.26 +flux_led==0.28.27 # homeassistant.components.homekit fnvhash==0.1.0 @@ -501,7 +508,10 @@ googlemaps==2.5.1 greeclimate==1.0.2 # homeassistant.components.greeneye_monitor -greeneye_monitor==3.0.1 +greeneye_monitor==3.0.3 + +# homeassistant.components.pure_energie +gridnet==4.0.0 # homeassistant.components.growatt_server growattServer==1.1.0 @@ -522,7 +532,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.52.0 +hass-nabucasa==0.54.0 # homeassistant.components.tasmota hatasmota==0.3.1 @@ -540,13 +550,13 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.12 +holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220203.1 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.10 +# homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect homeconnect==0.6.3 @@ -586,7 +596,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.6 +intellifire4py==0.9.9 # homeassistant.components.iotawatt iotawattpy==0.1.0 @@ -654,8 +664,11 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 +# homeassistant.components.moehlenhoff_alpha2 +moehlenhoff-alpha2==1.1.2 + # homeassistant.components.motion_blinds -motionblinds==0.5.12 +motionblinds==0.5.13 # homeassistant.components.motioneye motioneye-client==0.3.12 @@ -734,7 +747,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.ovo_energy -ovoenergy==1.1.12 +ovoenergy==1.2.0 # homeassistant.components.p1_monitor p1monitor==1.0.1 @@ -774,7 +787,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 @@ -783,7 +796,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.8.5 +plugwise==0.16.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -848,7 +861,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.1 # homeassistant.components.tibber -pyTibber==0.21.7 +pyTibber==0.22.1 # homeassistant.components.nextbus py_nextbusnext==0.1.5 @@ -878,7 +891,7 @@ pyatmo==6.2.4 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.9 +pyaussiebb==0.0.11 # homeassistant.components.balboa pybalboa==0.13 @@ -908,19 +921,19 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==86 +pydeconz==87 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 # homeassistant.components.econet -pyeconet==0.1.14 +pyeconet==0.1.15 # homeassistant.components.efergy -pyefergy==0.1.5 +pyefergy==22.1.1 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -953,7 +966,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.2 +pyfritzhome==0.6.4 # homeassistant.components.fronius pyfronius==0.7.1 @@ -969,7 +982,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.11.1 +pyhaversion==22.02.0 # homeassistant.components.heos pyheos==0.7.2 @@ -984,7 +997,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.icloud -pyicloud==0.10.2 +pyicloud==1.0.0 # homeassistant.components.insteon pyinsteon==1.0.16 @@ -998,6 +1011,9 @@ pyipp==0.11.0 # homeassistant.components.iqvia pyiqvia==2021.11.0 +# homeassistant.components.iss +pyiss==1.0.1 + # homeassistant.components.isy994 pyisy==3.0.1 @@ -1071,7 +1087,7 @@ pymysensors==0.22.1 pynetgear==0.9.1 # homeassistant.components.nina -pynina==0.1.4 +pynina==0.1.7 # homeassistant.components.nuki pynuki==1.5.2 @@ -1106,7 +1122,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.2 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1115,7 +1131,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.13 +pypck==0.7.14 # homeassistant.components.plaato pyplaato==0.0.15 @@ -1145,7 +1161,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sensibo -pysensibo==1.0.3 +pysensibo==1.0.7 # homeassistant.components.serial # homeassistant.components.zha @@ -1203,7 +1219,7 @@ python-juicenet==1.0.2 python-kasa==0.4.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.9.2 +python-miio==0.5.10 # homeassistant.components.nest python-nest==4.2.0 @@ -1218,14 +1234,11 @@ python-picnic-api==1.1.0 python-smarttub==0.0.29 # homeassistant.components.songpal -python-songpal==0.12 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.awair python_awair==0.2.1 @@ -1236,7 +1249,7 @@ pytile==2022.02.0 pytraccar==0.10.0 # homeassistant.components.tradfri -pytradfri[async]==8.0.1 +pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation @@ -1249,7 +1262,7 @@ pyudev==0.22.0 pyunifiprotect==3.2.0 # homeassistant.components.uptimerobot -pyuptimerobot==21.11.0 +pyuptimerobot==22.2.0 # homeassistant.components.vera pyvera==0.3.13 @@ -1272,17 +1285,23 @@ pywemo==0.7.0 # homeassistant.components.wilight pywilight==0.0.70 +# homeassistant.components.wiz +pywizlight==0.5.13 + # homeassistant.components.zerproc pyzerproc==0.4.8 # homeassistant.components.rachio rachiopy==1.0.3 +# homeassistant.components.radio_browser +radios==0.1.0 + # homeassistant.components.rainmachine regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.8 +renault-api==0.1.9 # homeassistant.components.python_script restrictedpython==5.2 @@ -1294,7 +1313,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.12.0 +rokuecp==0.14.1 # homeassistant.components.roomba roombapy==1.6.5 @@ -1315,7 +1334,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.6.0 +samsungtvws==1.7.0 # homeassistant.components.dhcp scapy==2.4.5 @@ -1325,10 +1344,10 @@ screenlogicpy==0.5.4 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.9.6 +sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.3 +sentry-sdk==1.5.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -1342,9 +1361,6 @@ simplisafe-python==2022.02.1 # homeassistant.components.slack slackclient==2.5.0 -# homeassistant.components.sleepiq -sleepyq==0.8.1 - # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 @@ -1355,7 +1371,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.2 +soco==0.26.3 # homeassistant.components.solaredge solaredge==0.0.2 @@ -1369,9 +1385,6 @@ somecomfort==0.8.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 -# homeassistant.components.sonarr -sonarr==0.3.0 - # homeassistant.components.marytts speak2mary==1.4.0 @@ -1390,7 +1403,7 @@ spotipy==2.19.0 sqlalchemy==1.4.27 # homeassistant.components.srp_energy -srpenergy==1.3.2 +srpenergy==1.3.6 # homeassistant.components.starline starline==0.1.5 @@ -1408,7 +1421,7 @@ stookalert==0.1.4 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.3.12 +subarulink==0.4.2 # homeassistant.components.solarlog sunwatcher==0.2.1 @@ -1417,7 +1430,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridge==2.2.3 +systembridge==2.3.1 # homeassistant.components.tailscale tailscale==0.2.0 @@ -1426,7 +1439,7 @@ tailscale==0.2.0 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.12 +tesla-powerwall==0.3.17 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 @@ -1438,13 +1451,13 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.1 +total_connect_client==2022.2.1 # homeassistant.components.transmission transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 @@ -1455,6 +1468,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 @@ -1469,19 +1485,20 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru +# homeassistant.components.zwave_me url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.9.0 +vallox-websocket-api==2.11.0 # homeassistant.components.rdw vehicle==0.3.1 # homeassistant.components.velbus -velbus-aio==2022.2.1 +velbus-aio==2022.2.4 # homeassistant.components.venstar venstarcolortouch==0.15 @@ -1515,7 +1532,7 @@ whois==0.9.13 wiffi==1.1.0 # homeassistant.components.withings -withings-api==2.3.2 +withings-api==2.4.0 # homeassistant.components.wled wled==0.13.0 @@ -1538,22 +1555,22 @@ xknx==0.19.2 xmltodict==0.12.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.7 +yalesmartalarmclient==0.3.8 # homeassistant.components.august yalexs==1.1.22 # homeassistant.components.yeelight -yeelight==0.7.8 +yeelight==0.7.9 # homeassistant.components.youless youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.66 +zha-quirks==0.0.67 # homeassistant.components.zha zigpy-deconz==0.14.0 @@ -1562,7 +1579,7 @@ zigpy-deconz==0.14.0 zigpy-xbee==0.14.0 # homeassistant.components.zha -zigpy-zigate==0.7.3 +zigpy-zigate==0.8.0 # homeassistant.components.zha zigpy-znp==0.7.0 @@ -1571,4 +1588,7 @@ zigpy-znp==0.7.0 zigpy==0.43.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.34.0 +zwave-js-server-python==0.35.1 + +# homeassistant.components.zwave_me +zwave_me_ws==0.2.1 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 2e37b2175ba94..ca7828267b6ea 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.12b0 +black==22.1.0 codespell==2.1.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 diff --git a/script/bootstrap b/script/bootstrap index b641ec7e8c038..5040a322b6205 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 +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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 872f2d0c7a8ec..fe8962e4f1e4d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -27,6 +27,7 @@ "envirophat", "evdev", "face_recognition", + "homeassistant-pyozw", "i2csense", "opencv-python-headless", "pybluez", @@ -76,7 +77,7 @@ # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.43.0 +grpcio==1.44.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, @@ -94,10 +95,6 @@ typing==1000000000.0.0 uuid==1000000000.0.0 -# Temporary constraint on pandas, to unblock 2021.7 releases -# until we have fixed the wheels builds for newer versions. -pandas==1.3.0 - # regex causes segfault with version 2021.8.27 # https://bitbucket.org/mrabarnett/mrab-regex/issues/421/2021827-results-in-fatal-python-error # This is fixed in 2021.8.28 @@ -111,6 +108,10 @@ h11==0.12.0 httpcore==0.14.5 +# Ensure we have a hyperframe version that works in Python 3.10 +# 5.2.0 fixed a collections abc deprecation +hyperframe>=5.2.0 + # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 @@ -121,8 +122,8 @@ python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues -# https://github.com/home-assistant/core/pull/64792 -multidict<6.0.0 +# https://github.com/home-assistant/core/pull/67046 +multidict>=6.0.2 """ IGNORE_PRE_COMMIT_HOOK_ID = ( @@ -167,7 +168,7 @@ def explore_module(package, explore_children): def core_requirements(): - """Gather core requirements out of setup.py.""" + """Gather core requirements out of setup.cfg.""" parser = configparser.ConfigParser() parser.read("setup.cfg") return parser["options"]["install_requires"].strip().split("\n") diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index ac3d3ce8a8579..c6a9799a502c6 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -45,6 +45,11 @@ metadata, ] +ALL_PLUGIN_NAMES = [ + plugin.__name__.rsplit(".", maxsplit=1)[-1] + for plugin in (*INTEGRATION_PLUGINS, *HASS_PLUGINS) +] + def valid_integration_path(integration_path): """Test if it's a valid integration.""" @@ -55,6 +60,17 @@ def valid_integration_path(integration_path): return path +def validate_plugins(plugin_names: str) -> list[str]: + """Split and validate plugin names.""" + all_plugin_names = set(ALL_PLUGIN_NAMES) + plugins = plugin_names.split(",") + for plugin in plugins: + if plugin not in all_plugin_names: + raise argparse.ArgumentTypeError(f"{plugin} is not a valid plugin name") + + return plugins + + def get_config() -> Config: """Return config.""" parser = argparse.ArgumentParser(description="Hassfest") @@ -72,6 +88,13 @@ def get_config() -> Config: action="store_true", help="Validate requirements", ) + parser.add_argument( + "-p", + "--plugins", + type=validate_plugins, + default=ALL_PLUGIN_NAMES, + help="Comma-separate list of plugins to run. Valid plugin names: %(default)s", + ) parsed = parser.parse_args() if parsed.action is None: @@ -93,6 +116,7 @@ def get_config() -> Config: specific_integrations=parsed.integration_path, action=parsed.action, requirements=parsed.requirements, + plugins=set(parsed.plugins), ) @@ -119,9 +143,12 @@ def main(): plugins += HASS_PLUGINS for plugin in plugins: + plugin_name = plugin.__name__.rsplit(".", maxsplit=1)[-1] + if plugin_name not in config.plugins: + continue try: start = monotonic() - print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True) + print(f"Validating {plugin_name}...", end="", flush=True) if ( plugin is requirements and config.requirements @@ -163,6 +190,9 @@ def main(): if config.action == "generate": for plugin in plugins: + plugin_name = plugin.__name__.rsplit(".", maxsplit=1)[-1] + if plugin_name not in config.plugins: + continue if hasattr(plugin, "generate"): plugin.generate(integrations, config) return 0 diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 91bd81efef5c8..cf8fb02b98900 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -10,7 +10,7 @@ # https://github.com/blog/2392-introducing-code-owners # Home Assistant Core -setup.py @home-assistant/core +setup.cfg @home-assistant/core homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index ec0b437186e73..7c259adbfa317 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -20,46 +20,13 @@ # They were violating when we introduced this check # Need to be fixed in a future PR. ALLOWED_IGNORE_VIOLATIONS = { - ("ambient_station", "config_flow.py"), - ("cast", "config_flow.py"), - ("daikin", "config_flow.py"), - ("doorbird", "config_flow.py"), ("doorbird", "logbook.py"), - ("elkm1", "config_flow.py"), ("elkm1", "scene.py"), ("fibaro", "scene.py"), - ("hangouts", "config_flow.py"), - ("harmony", "config_flow.py"), - ("huawei_lte", "config_flow.py"), - ("ifttt", "config_flow.py"), - ("ios", "config_flow.py"), - ("iqvia", "config_flow.py"), - ("konnected", "config_flow.py"), ("lcn", "scene.py"), - ("life360", "config_flow.py"), - ("lifx", "config_flow.py"), ("lutron", "scene.py"), - ("mobile_app", "config_flow.py"), - ("nest", "config_flow.py"), - ("plaato", "config_flow.py"), - ("point", "config_flow.py"), - ("rachio", "config_flow.py"), - ("sense", "config_flow.py"), - ("sms", "config_flow.py"), - ("solarlog", "config_flow.py"), - ("sonos", "config_flow.py"), - ("speedtestdotnet", "config_flow.py"), - ("spider", "config_flow.py"), - ("starline", "config_flow.py"), - ("tado", "config_flow.py"), - ("totalconnect", "config_flow.py"), - ("tradfri", "config_flow.py"), - ("tuya", "config_flow.py"), ("tuya", "scene.py"), - ("upnp", "config_flow.py"), ("velux", "scene.py"), - ("wemo", "config_flow.py"), - ("wiffi", "config_flow.py"), } diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index c746c64e46f0d..1aca6a1f68da1 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -1,7 +1,8 @@ """Generate dhcp file.""" from __future__ import annotations -import json +import pprint +import re from .model import Config, Integration @@ -10,10 +11,11 @@ To update, run python3 -m script.hassfest \"\"\" +from __future__ import annotations # fmt: off -DHCP = {} +DHCP: list[dict[str, str | bool]] = {} """.strip() @@ -35,7 +37,14 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return BASE.format(json.dumps(match_list, indent=4)) + # JSON will format `True` as `true` + # re.sub for flake8 E128 + formatted = pprint.pformat(match_list) + formatted_aligned_continuation = re.sub(r"^\[\{", "[\n {", formatted) + formatted_align_indent = re.sub( + r"(?m)^ ", " ", formatted_aligned_continuation, flags=re.MULTILINE, count=0 + ) + return BASE.format(formatted_align_indent) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 54d4944cf7034..64239a7fae184 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -218,6 +218,7 @@ def verify_wildcard(value: str): str, verify_uppercase, verify_wildcard ), vol.Optional("hostname"): vol.All(str, verify_lowercase), + vol.Optional("registered_devices"): cv.boolean, } ) ], @@ -244,14 +245,17 @@ def verify_wildcard(value: str): vol.Optional("dependencies"): [str], vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], + vol.Optional("loggers"): [str], vol.Optional("disabled"): str, vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), + vol.Optional("supported_brands"): vol.Schema({str: str}), } ) CUSTOM_INTEGRATION_MANIFEST_SCHEMA = MANIFEST_SCHEMA.extend( { vol.Optional("version"): vol.All(str, verify_version), + vol.Remove("supported_brands"): dict, } ) @@ -305,6 +309,13 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No ): integration.add_error("manifest", "Domain is missing an IoT Class") + for domain, _name in integration.manifest.get("supported_brands", {}).items(): + if (core_components_dir / domain).exists(): + integration.add_warning( + "manifest", + f"Supported brand domain {domain} collides with built-in core integration", + ) + if not integration.core: validate_version(integration) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 69810686cc16d..7006c1e603260 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -32,6 +32,7 @@ class Config: requirements: bool = attr.ib() errors: list[Error] = attr.ib(factory=list) cache: dict[str, Any] = attr.ib(factory=dict) + plugins: set[str] = attr.ib(factory=set) def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d2bd437c2d9d5..ef29562578e73 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,69 +16,220 @@ # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.blueprint.*", - "homeassistant.components.cloud.*", - "homeassistant.components.config.*", - "homeassistant.components.conversation.*", - "homeassistant.components.deconz.*", - "homeassistant.components.demo.*", - "homeassistant.components.denonavr.*", - "homeassistant.components.evohome.*", - "homeassistant.components.fireservicerota.*", - "homeassistant.components.firmata.*", - "homeassistant.components.freebox.*", - "homeassistant.components.geniushub.*", - "homeassistant.components.google_assistant.*", - "homeassistant.components.gree.*", - "homeassistant.components.harmony.*", - "homeassistant.components.hassio.*", - "homeassistant.components.here_travel_time.*", - "homeassistant.components.home_plus_control.*", - "homeassistant.components.homekit.*", - "homeassistant.components.homekit_controller.*", - "homeassistant.components.honeywell.*", - "homeassistant.components.icloud.*", - "homeassistant.components.influxdb.*", - "homeassistant.components.input_datetime.*", - "homeassistant.components.isy994.*", - "homeassistant.components.izone.*", - "homeassistant.components.konnected.*", - "homeassistant.components.kostal_plenticore.*", - "homeassistant.components.litterrobot.*", - "homeassistant.components.lovelace.*", - "homeassistant.components.lutron_caseta.*", - "homeassistant.components.lyric.*", - "homeassistant.components.melcloud.*", - "homeassistant.components.meteo_france.*", - "homeassistant.components.minecraft_server.*", - "homeassistant.components.mobile_app.*", - "homeassistant.components.nest.legacy.*", - "homeassistant.components.netgear.*", - "homeassistant.components.nilu.*", - "homeassistant.components.nzbget.*", - "homeassistant.components.omnilogic.*", - "homeassistant.components.onvif.*", - "homeassistant.components.ozw.*", - "homeassistant.components.philips_js.*", - "homeassistant.components.plex.*", - "homeassistant.components.profiler.*", - "homeassistant.components.solaredge.*", - "homeassistant.components.sonos.*", - "homeassistant.components.spotify.*", - "homeassistant.components.system_health.*", - "homeassistant.components.telegram_bot.*", - "homeassistant.components.template.*", - "homeassistant.components.toon.*", - "homeassistant.components.unifi.*", - "homeassistant.components.upnp.*", - "homeassistant.components.vizio.*", - "homeassistant.components.withings.*", - "homeassistant.components.xbox.*", - "homeassistant.components.xiaomi_aqara.*", - "homeassistant.components.xiaomi_miio.*", - "homeassistant.components.yeelight.*", - "homeassistant.components.zha.*", - "homeassistant.components.zwave.*", + "homeassistant.components.blueprint.importer", + "homeassistant.components.blueprint.models", + "homeassistant.components.blueprint.websocket_api", + "homeassistant.components.cloud.client", + "homeassistant.components.cloud.http_api", + "homeassistant.components.conversation", + "homeassistant.components.conversation.default_agent", + "homeassistant.components.deconz.alarm_control_panel", + "homeassistant.components.deconz.binary_sensor", + "homeassistant.components.deconz.climate", + "homeassistant.components.deconz.cover", + "homeassistant.components.deconz.fan", + "homeassistant.components.deconz.light", + "homeassistant.components.deconz.lock", + "homeassistant.components.deconz.logbook", + "homeassistant.components.deconz.number", + "homeassistant.components.deconz.sensor", + "homeassistant.components.deconz.siren", + "homeassistant.components.deconz.switch", + "homeassistant.components.denonavr.config_flow", + "homeassistant.components.denonavr.media_player", + "homeassistant.components.denonavr.receiver", + "homeassistant.components.evohome", + "homeassistant.components.evohome.climate", + "homeassistant.components.evohome.water_heater", + "homeassistant.components.google_assistant.helpers", + "homeassistant.components.google_assistant.http", + "homeassistant.components.google_assistant.report_state", + "homeassistant.components.google_assistant.trait", + "homeassistant.components.gree.climate", + "homeassistant.components.gree.switch", + "homeassistant.components.harmony", + "homeassistant.components.harmony.config_flow", + "homeassistant.components.harmony.data", + "homeassistant.components.hassio", + "homeassistant.components.hassio.auth", + "homeassistant.components.hassio.binary_sensor", + "homeassistant.components.hassio.ingress", + "homeassistant.components.hassio.sensor", + "homeassistant.components.hassio.system_health", + "homeassistant.components.hassio.websocket_api", + "homeassistant.components.here_travel_time.sensor", + "homeassistant.components.home_plus_control", + "homeassistant.components.home_plus_control.api", + "homeassistant.components.homekit.aidmanager", + "homeassistant.components.homekit.config_flow", + "homeassistant.components.homekit.util", + "homeassistant.components.honeywell.climate", + "homeassistant.components.icloud", + "homeassistant.components.icloud.account", + "homeassistant.components.icloud.device_tracker", + "homeassistant.components.icloud.sensor", + "homeassistant.components.influxdb", + "homeassistant.components.input_datetime", + "homeassistant.components.izone.climate", + "homeassistant.components.konnected", + "homeassistant.components.konnected.config_flow", + "homeassistant.components.kostal_plenticore.helper", + "homeassistant.components.kostal_plenticore.select", + "homeassistant.components.kostal_plenticore.sensor", + "homeassistant.components.kostal_plenticore.switch", + "homeassistant.components.lovelace", + "homeassistant.components.lovelace.dashboard", + "homeassistant.components.lovelace.resources", + "homeassistant.components.lovelace.websocket", + "homeassistant.components.lutron_caseta", + "homeassistant.components.lutron_caseta.device_trigger", + "homeassistant.components.lutron_caseta.switch", + "homeassistant.components.lyric.climate", + "homeassistant.components.lyric.config_flow", + "homeassistant.components.lyric.sensor", + "homeassistant.components.melcloud", + "homeassistant.components.melcloud.climate", + "homeassistant.components.meteo_france.sensor", + "homeassistant.components.meteo_france.weather", + "homeassistant.components.minecraft_server", + "homeassistant.components.minecraft_server.helpers", + "homeassistant.components.minecraft_server.sensor", + "homeassistant.components.netgear", + "homeassistant.components.netgear.config_flow", + "homeassistant.components.netgear.device_tracker", + "homeassistant.components.netgear.router", + "homeassistant.components.nilu.air_quality", + "homeassistant.components.nzbget", + "homeassistant.components.nzbget.config_flow", + "homeassistant.components.nzbget.coordinator", + "homeassistant.components.nzbget.switch", + "homeassistant.components.omnilogic.common", + "homeassistant.components.omnilogic.sensor", + "homeassistant.components.omnilogic.switch", + "homeassistant.components.onvif.base", + "homeassistant.components.onvif.binary_sensor", + "homeassistant.components.onvif.button", + "homeassistant.components.onvif.camera", + "homeassistant.components.onvif.config_flow", + "homeassistant.components.onvif.device", + "homeassistant.components.onvif.event", + "homeassistant.components.onvif.models", + "homeassistant.components.onvif.parsers", + "homeassistant.components.onvif.sensor", + "homeassistant.components.ozw", + "homeassistant.components.ozw.climate", + "homeassistant.components.ozw.entity", + "homeassistant.components.philips_js", + "homeassistant.components.philips_js.config_flow", + "homeassistant.components.philips_js.device_trigger", + "homeassistant.components.philips_js.light", + "homeassistant.components.philips_js.media_player", + "homeassistant.components.plex.media_player", + "homeassistant.components.profiler", + "homeassistant.components.solaredge.config_flow", + "homeassistant.components.solaredge.coordinator", + "homeassistant.components.solaredge.sensor", + "homeassistant.components.sonos", + "homeassistant.components.sonos.alarms", + "homeassistant.components.sonos.binary_sensor", + "homeassistant.components.sonos.diagnostics", + "homeassistant.components.sonos.entity", + "homeassistant.components.sonos.favorites", + "homeassistant.components.sonos.helpers", + "homeassistant.components.sonos.media_browser", + "homeassistant.components.sonos.media_player", + "homeassistant.components.sonos.number", + "homeassistant.components.sonos.sensor", + "homeassistant.components.sonos.speaker", + "homeassistant.components.sonos.statistics", + "homeassistant.components.system_health", + "homeassistant.components.telegram_bot.polling", + "homeassistant.components.template.number", + "homeassistant.components.template.sensor", + "homeassistant.components.toon", + "homeassistant.components.toon.config_flow", + "homeassistant.components.toon.models", + "homeassistant.components.unifi", + "homeassistant.components.unifi.config_flow", + "homeassistant.components.unifi.device_tracker", + "homeassistant.components.unifi.diagnostics", + "homeassistant.components.unifi.unifi_entity_base", + "homeassistant.components.upnp", + "homeassistant.components.upnp.binary_sensor", + "homeassistant.components.upnp.config_flow", + "homeassistant.components.upnp.device", + "homeassistant.components.upnp.sensor", + "homeassistant.components.vizio.config_flow", + "homeassistant.components.vizio.media_player", + "homeassistant.components.withings", + "homeassistant.components.withings.binary_sensor", + "homeassistant.components.withings.common", + "homeassistant.components.withings.config_flow", + "homeassistant.components.xbox", + "homeassistant.components.xbox.base_sensor", + "homeassistant.components.xbox.binary_sensor", + "homeassistant.components.xbox.browse_media", + "homeassistant.components.xbox.media_source", + "homeassistant.components.xbox.sensor", + "homeassistant.components.xiaomi_aqara", + "homeassistant.components.xiaomi_aqara.binary_sensor", + "homeassistant.components.xiaomi_aqara.lock", + "homeassistant.components.xiaomi_aqara.sensor", + "homeassistant.components.xiaomi_miio", + "homeassistant.components.xiaomi_miio.air_quality", + "homeassistant.components.xiaomi_miio.binary_sensor", + "homeassistant.components.xiaomi_miio.device", + "homeassistant.components.xiaomi_miio.device_tracker", + "homeassistant.components.xiaomi_miio.fan", + "homeassistant.components.xiaomi_miio.humidifier", + "homeassistant.components.xiaomi_miio.light", + "homeassistant.components.xiaomi_miio.sensor", + "homeassistant.components.xiaomi_miio.switch", + "homeassistant.components.yeelight", + "homeassistant.components.yeelight.light", + "homeassistant.components.yeelight.scanner", + "homeassistant.components.zha.alarm_control_panel", + "homeassistant.components.zha.api", + "homeassistant.components.zha.binary_sensor", + "homeassistant.components.zha.button", + "homeassistant.components.zha.climate", + "homeassistant.components.zha.config_flow", + "homeassistant.components.zha.core.channels", + "homeassistant.components.zha.core.channels.base", + "homeassistant.components.zha.core.channels.closures", + "homeassistant.components.zha.core.channels.general", + "homeassistant.components.zha.core.channels.homeautomation", + "homeassistant.components.zha.core.channels.hvac", + "homeassistant.components.zha.core.channels.lighting", + "homeassistant.components.zha.core.channels.lightlink", + "homeassistant.components.zha.core.channels.manufacturerspecific", + "homeassistant.components.zha.core.channels.measurement", + "homeassistant.components.zha.core.channels.protocol", + "homeassistant.components.zha.core.channels.security", + "homeassistant.components.zha.core.channels.smartenergy", + "homeassistant.components.zha.core.decorators", + "homeassistant.components.zha.core.device", + "homeassistant.components.zha.core.discovery", + "homeassistant.components.zha.core.gateway", + "homeassistant.components.zha.core.group", + "homeassistant.components.zha.core.helpers", + "homeassistant.components.zha.core.registries", + "homeassistant.components.zha.core.store", + "homeassistant.components.zha.core.typing", + "homeassistant.components.zha.cover", + "homeassistant.components.zha.device_action", + "homeassistant.components.zha.device_tracker", + "homeassistant.components.zha.entity", + "homeassistant.components.zha.fan", + "homeassistant.components.zha.light", + "homeassistant.components.zha.lock", + "homeassistant.components.zha.select", + "homeassistant.components.zha.sensor", + "homeassistant.components.zha.siren", + "homeassistant.components.zha.switch", + "homeassistant.components.zwave", + "homeassistant.components.zwave.migration", + "homeassistant.components.zwave.node_entity", ] # Component modules which should set no_implicit_reexport = true. @@ -90,7 +241,7 @@ HEADER: Final = """ # Automatically generated by hassfest. # -# To update, run python3 -m script.hassfest +# To update, run python3 -m script.hassfest -p mypy_config """.lstrip() @@ -133,6 +284,19 @@ ] +def _strict_module_in_ignore_list( + module: str, ignored_modules_set: set[str] +) -> str | None: + if module in ignored_modules_set: + return module + if module.endswith("*"): + module = module[:-1] + for ignored_module in ignored_modules_set: + if ignored_module.startswith(module): + return ignored_module + return None + + def generate_and_validate(config: Config) -> str: """Validate and generate mypy config.""" @@ -165,9 +329,10 @@ def generate_and_validate(config: Config) -> str: config.add_error( "mypy_config", f"Only components should be added: {module}" ) - if module in ignored_modules_set: + if ignored_module := _strict_module_in_ignore_list(module, ignored_modules_set): config.add_error( - "mypy_config", f"Module '{module}' is in ignored list in mypy_config.py" + "mypy_config", + f"Module '{ignored_module}' is in ignored list in mypy_config.py", ) # Validate that all modules exist. diff --git a/script/pip_check b/script/pip_check index 1b2be96132120..fa217e8986631 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=13 +DEPENDENCY_CONFLICTS=6 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) diff --git a/script/setup b/script/setup index f827c3a373f5a..210779eec4580 100755 --- a/script/setup +++ b/script/setup @@ -24,7 +24,7 @@ fi script/bootstrap pre-commit install -python3 -m pip install -e . --constraint homeassistant/package_constraints.txt +python3 -m pip install -e . --constraint homeassistant/package_constraints.txt --use-deprecated=legacy-resolver hass --script ensure_config -c config diff --git a/script/translations/develop.py b/script/translations/develop.py index f59b9b9c7cb63..2f9966afc2964 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -75,7 +75,13 @@ def substitute_reference(value, flattened_translations): new = value for key in matches: if key in flattened_translations: - new = new.replace(f"[%key:{key}%]", flattened_translations[key]) + new = new.replace( + f"[%key:{key}%]", + # New value can also be a substitution reference + substitute_reference( + flattened_translations[key], flattened_translations + ), + ) else: print(f"Invalid substitution key '{key}' found in string '{value}'") sys.exit(1) diff --git a/script/version_bump.py b/script/version_bump.py index 6044cdb277c40..d714c5183b7ed 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -131,6 +131,23 @@ def write_version_metadata(version: Version) -> None: fp.write(content) +def write_ci_workflow(version: Version) -> None: + """Update ci workflow with new version.""" + with open(".github/workflows/ci.yaml") as fp: + content = fp.read() + + short_version = ".".join(str(version).split(".", maxsplit=2)[:2]) + content = re.sub( + r"(\n\W+HA_SHORT_VERSION: )\d{4}\.\d{1,2}\n", + f"\\g<1>{short_version}\n", + content, + count=1, + ) + + with open(".github/workflows/ci.yaml", "w") as fp: + fp.write(content) + + def main(): """Execute script.""" parser = argparse.ArgumentParser(description="Bump version of Home Assistant") @@ -154,6 +171,8 @@ def main(): write_version(bumped) write_version_metadata(bumped) + write_ci_workflow(bumped) + print(bumped) if not arguments.commit: return diff --git a/setup.cfg b/setup.cfg index 9ce81a08a10e7..d72829b574e5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.2.9 +version = 2022.3.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 @@ -36,7 +36,7 @@ install_requires = async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 - awesomeversion==22.1.0 + awesomeversion==22.2.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 @@ -48,7 +48,7 @@ install_requires = PyJWT==2.1.0 # PyJWT has loose dependency. We want the latest one. cryptography==35.0.0 - pip>=8.0.3,<20.3 + pip>=21.0,<22.1 python-slugify==4.0.1 pyyaml==6.0 requests==2.27.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 69bf65dd8a4bd..0000000000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Entry point for setuptools. Required for editable installs. -TODO: Remove file after updating to pip 21.3 -""" -from setuptools import setup - -setup() diff --git a/tests/common.py b/tests/common.py index 3ea4cde2cecb0..bdebc7217a7ae 100644 --- a/tests/common.py +++ b/tests/common.py @@ -44,7 +44,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, State +from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant from homeassistant.helpers import ( area_registry, device_registry, @@ -583,6 +583,7 @@ def __init__( async_migrate_entry=None, async_remove_entry=None, partial_manifest=None, + async_remove_config_entry_device=None, ): """Initialize the mock module.""" self.__name__ = f"homeassistant.components.{domain}" @@ -624,6 +625,9 @@ def __init__( if async_remove_entry is not None: self.async_remove_entry = async_remove_entry + if async_remove_config_entry_device is not None: + self.async_remove_config_entry_device = async_remove_config_entry_device + def mock_manifest(self): """Generate a mock manifest to represent this module.""" return { @@ -931,11 +935,39 @@ def mock_restore_cache(hass, states): last_states = {} for state in states: restored_state = state.as_dict() - restored_state["attributes"] = json.loads( - json.dumps(restored_state["attributes"], cls=JSONEncoder) + restored_state = { + **restored_state, + "attributes": json.loads( + json.dumps(restored_state["attributes"], cls=JSONEncoder) + ), + } + last_states[state.entity_id] = restore_state.StoredState.from_dict( + {"state": restored_state, "last_seen": now} ) - last_states[state.entity_id] = restore_state.StoredState( - State.from_dict(restored_state), now + data.last_states = last_states + _LOGGER.debug("Restore cache: %s", data.last_states) + assert len(data.last_states) == len(states), f"Duplicate entity_id? {states}" + + hass.data[key] = data + + +def mock_restore_cache_with_extra_data(hass, states): + """Mock the DATA_RESTORE_CACHE.""" + key = restore_state.DATA_RESTORE_STATE_TASK + data = restore_state.RestoreStateData(hass) + now = date_util.utcnow() + + last_states = {} + for state, extra_data in states: + restored_state = state.as_dict() + restored_state = { + **restored_state, + "attributes": json.loads( + json.dumps(restored_state["attributes"], cls=JSONEncoder) + ), + } + last_states[state.entity_id] = restore_state.StoredState.from_dict( + {"state": restored_state, "extra_data": extra_data, "last_seen": now} ) data.last_states = last_states _LOGGER.debug("Restore cache: %s", data.last_states) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 8a9a40e321718..d24849e100601 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -182,7 +182,7 @@ async def test_api_increase_color_temp(hass, result, initial): @pytest.mark.parametrize( "domain,payload,source_list,idx", [ - ("media_player", "GAME CONSOLE", ["tv", "game console"], 1), + ("media_player", "GAME CONSOLE", ["tv", "game console", 10000], 1), ("media_player", "SATELLITE TV", ["satellite-tv", "game console"], 0), ("media_player", "SATELLITE TV", ["satellite_tv", "game console"], 0), ("media_player", "BAD DEVICE", ["satellite_tv", "game console"], None), diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index d6c32996330aa..f15fa860c7b20 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -13,6 +13,9 @@ SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" +APPLICATION_ID_SESSION_OPEN = ( + "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebf" +) REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC" BUILTIN_AUTH_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.TEST" @@ -102,6 +105,16 @@ def mock_service(call): "text": "LaunchRequest has been received.", } }, + APPLICATION_ID_SESSION_OPEN: { + "speech": { + "type": "plain", + "text": "LaunchRequest has been received.", + }, + "reprompt": { + "type": "plain", + "text": "LaunchRequest has been received.", + }, + }, } }, ) @@ -139,6 +152,36 @@ async def test_intent_launch_request(alexa_client): data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "LaunchRequest has been received." + assert data.get("response", {}).get("shouldEndSession") + + +async def test_intent_launch_request_with_session_open(alexa_client): + """Test the launch of a request.""" + data = { + "version": "1.0", + "session": { + "new": True, + "sessionId": SESSION_ID, + "application": {"applicationId": APPLICATION_ID_SESSION_OPEN}, + "attributes": {}, + "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, + }, + "request": { + "type": "LaunchRequest", + "requestId": REQUEST_ID, + "timestamp": "2015-05-13T12:34:56Z", + }, + } + req = await _intent_req(alexa_client, data) + assert req.status == HTTPStatus.OK + data = await req.json() + text = data.get("response", {}).get("outputSpeech", {}).get("text") + assert text == "LaunchRequest has been received." + text = ( + data.get("response", {}).get("reprompt", {}).get("outputSpeech", {}).get("text") + ) + assert text == "LaunchRequest has been received." + assert not data.get("response", {}).get("shouldEndSession") async def test_intent_launch_request_not_configured(alexa_client): diff --git a/tests/components/apns/__init__.py b/tests/components/apns/__init__.py deleted file mode 100644 index 42c980a62a7d1..0000000000000 --- a/tests/components/apns/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the apns component.""" diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py deleted file mode 100644 index ad55a5697ad2b..0000000000000 --- a/tests/components/apns/test_notify.py +++ /dev/null @@ -1,395 +0,0 @@ -"""The tests for the APNS component.""" -import io -from unittest.mock import Mock, mock_open, patch - -from apns2.errors import Unregistered -import pytest -import yaml - -import homeassistant.components.apns.notify as apns -import homeassistant.components.notify as notify -from homeassistant.core import State -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component - -CONFIG = { - notify.DOMAIN: { - "platform": "apns", - "name": "test_app", - "topic": "testapp.appname", - "cert_file": "test_app.pem", - } -} - - -@pytest.fixture(scope="module", autouse=True) -def mock_apns_notify_open(): - """Mock builtins.open for apns.notify.""" - with patch("homeassistant.components.apns.notify.open", mock_open(), create=True): - yield - - -@patch("os.path.isfile", Mock(return_value=True)) -@patch("os.access", Mock(return_value=True)) -async def _setup_notify(hass_): - assert isinstance(apns.load_yaml_config_file, Mock), "Found unmocked load_yaml" - - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass_, notify.DOMAIN, CONFIG) - assert handle_config[notify.DOMAIN] - - -@patch("os.path.isfile", return_value=True) -@patch("os.access", return_value=True) -async def test_apns_setup_full(mock_access, mock_isfile, hass): - """Test setup with all data.""" - config = { - "notify": { - "platform": "apns", - "name": "test_app", - "sandbox": "True", - "topic": "testapp.appname", - "cert_file": "test_app.pem", - } - } - - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - assert handle_config[notify.DOMAIN] - - -async def test_apns_setup_missing_name(hass): - """Test setup with missing name.""" - config = { - "notify": { - "platform": "apns", - "topic": "testapp.appname", - "cert_file": "test_app.pem", - } - } - with assert_setup_component(0) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - assert not handle_config[notify.DOMAIN] - - -async def test_apns_setup_missing_certificate(hass): - """Test setup with missing certificate.""" - config = { - "notify": { - "platform": "apns", - "name": "test_app", - "topic": "testapp.appname", - } - } - with assert_setup_component(0) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - assert not handle_config[notify.DOMAIN] - - -async def test_apns_setup_missing_topic(hass): - """Test setup with missing topic.""" - config = { - "notify": { - "platform": "apns", - "name": "test_app", - "cert_file": "test_app.pem", - } - } - with assert_setup_component(0) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - assert not handle_config[notify.DOMAIN] - - -@patch("homeassistant.components.apns.notify._write_device") -async def test_register_new_device(mock_write, hass): - """Test registering a new device with a name.""" - yaml_file = {5678: {"name": "test device 2"}} - - written_devices = [] - - def fake_write(_out, device): - """Fake write_device.""" - written_devices.append(device) - - mock_write.side_effect = fake_write - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - apns.DOMAIN, - "apns_test_app", - {"push_id": "1234", "name": "test device"}, - blocking=True, - ) - - assert len(written_devices) == 1 - assert written_devices[0].name == "test device" - - -@patch("homeassistant.components.apns.notify._write_device") -async def test_register_device_without_name(mock_write, hass): - """Test registering a without a name.""" - yaml_file = { - 1234: {"name": "test device 1", "tracking_device_id": "tracking123"}, - 5678: {"name": "test device 2", "tracking_device_id": "tracking456"}, - } - - written_devices = [] - - def fake_write(_out, device): - """Fake write_device.""" - written_devices.append(device) - - mock_write.side_effect = fake_write - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - apns.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True - ) - - devices = {dev.push_id: dev for dev in written_devices} - - test_device = devices.get("1234") - - assert test_device is not None - assert test_device.name is None - - -@patch("homeassistant.components.apns.notify._write_device") -async def test_update_existing_device(mock_write, hass): - """Test updating an existing device.""" - yaml_file = {1234: {"name": "test device 1"}, 5678: {"name": "test device 2"}} - - written_devices = [] - - def fake_write(_out, device): - """Fake write_device.""" - written_devices.append(device) - - mock_write.side_effect = fake_write - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - apns.DOMAIN, - "apns_test_app", - {"push_id": "1234", "name": "updated device 1"}, - blocking=True, - ) - - devices = {dev.push_id: dev for dev in written_devices} - - test_device_1 = devices.get("1234") - test_device_2 = devices.get("5678") - - assert test_device_1 is not None - assert test_device_2 is not None - - assert test_device_1.name == "updated device 1" - - -@patch("homeassistant.components.apns.notify._write_device") -async def test_update_existing_device_with_tracking_id(mock_write, hass): - """Test updating an existing device that has a tracking id.""" - yaml_file = { - 1234: {"name": "test device 1", "tracking_device_id": "tracking123"}, - 5678: {"name": "test device 2", "tracking_device_id": "tracking456"}, - } - - written_devices = [] - - def fake_write(_out, device): - """Fake write_device.""" - written_devices.append(device) - - mock_write.side_effect = fake_write - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - apns.DOMAIN, - "apns_test_app", - {"push_id": "1234", "name": "updated device 1"}, - blocking=True, - ) - - devices = {dev.push_id: dev for dev in written_devices} - - test_device_1 = devices.get("1234") - test_device_2 = devices.get("5678") - - assert test_device_1 is not None - assert test_device_2 is not None - - assert test_device_1.tracking_device_id == "tracking123" - assert test_device_2.tracking_device_id == "tracking456" - - -@patch("homeassistant.components.apns.notify.APNsClient") -async def test_send(mock_client, hass): - """Test updating an existing device.""" - send = mock_client.return_value.send_notification - - yaml_file = {1234: {"name": "test device 1"}} - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - "notify", - "test_app", - { - "message": "Hello", - "data": {"badge": 1, "sound": "test.mp3", "category": "testing"}, - }, - blocking=True, - ) - - assert send.called - assert len(send.mock_calls) == 1 - - target = send.mock_calls[0][1][0] - payload = send.mock_calls[0][1][1] - - assert target == "1234" - assert payload.alert == "Hello" - assert payload.badge == 1 - assert payload.sound == "test.mp3" - assert payload.category == "testing" - - -@patch("homeassistant.components.apns.notify.APNsClient") -async def test_send_when_disabled(mock_client, hass): - """Test updating an existing device.""" - send = mock_client.return_value.send_notification - - yaml_file = {1234: {"name": "test device 1", "disabled": True}} - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - "notify", - "test_app", - { - "message": "Hello", - "data": {"badge": 1, "sound": "test.mp3", "category": "testing"}, - }, - blocking=True, - ) - - assert not send.called - - -@patch("homeassistant.components.apns.notify.APNsClient") -async def test_send_with_state(mock_client, hass): - """Test updating an existing device.""" - send = mock_client.return_value.send_notification - - yaml_file = { - 1234: {"name": "test device 1", "tracking_device_id": "tracking123"}, - 5678: {"name": "test device 2", "tracking_device_id": "tracking456"}, - } - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ), patch("os.path.isfile", Mock(return_value=True)): - notify_service = await hass.async_add_executor_job( - apns.ApnsNotificationService, - hass, - "test_app", - "testapp.appname", - False, - "test_app.pem", - ) - - notify_service.device_state_changed_listener( - "device_tracker.tracking456", - State("device_tracker.tracking456", None), - State("device_tracker.tracking456", "home"), - ) - - notify_service.send_message(message="Hello", target="home") - - assert send.called - assert len(send.mock_calls) == 1 - - target = send.mock_calls[0][1][0] - payload = send.mock_calls[0][1][1] - - assert target == "5678" - assert payload.alert == "Hello" - - -@patch("homeassistant.components.apns.notify.APNsClient") -@patch("homeassistant.components.apns.notify._write_device") -async def test_disable_when_unregistered(mock_write, mock_client, hass): - """Test disabling a device when it is unregistered.""" - send = mock_client.return_value.send_notification - send.side_effect = Unregistered() - - yaml_file = { - 1234: {"name": "test device 1", "tracking_device_id": "tracking123"}, - 5678: {"name": "test device 2", "tracking_device_id": "tracking456"}, - } - - written_devices = [] - - def fake_write(_out, device): - """Fake write_device.""" - written_devices.append(device) - - mock_write.side_effect = fake_write - - with patch( - "homeassistant.components.apns.notify.load_yaml_config_file", - Mock(return_value=yaml_file), - ): - await _setup_notify(hass) - - assert await hass.services.async_call( - "notify", "test_app", {"message": "Hello"}, blocking=True - ) - - devices = {dev.push_id: dev for dev in written_devices} - - test_device_1 = devices.get("1234") - assert test_device_1 is not None - assert test_device_1.disabled is True - - -async def test_write_device(): - """Test writing device.""" - out = io.StringIO() - device = apns.ApnsDevice("123", "name", "track_id", True) - - apns._write_device(out, device) - data = yaml.safe_load(out.getvalue()) - assert data == { - 123: {"name": "name", "tracking_device_id": "track_id", "disabled": True} - } diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index b4811e5773989..ca617026d94a5 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -22,6 +22,7 @@ DMAP_SERVICE = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_touch-able._tcp.local.", @@ -32,6 +33,7 @@ RAOP_SERVICE = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_raop._tcp.local.", @@ -531,6 +533,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=None, @@ -549,6 +552,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.2", + addresses=["127.0.0.2"], hostname="mock_hostname", port=None, name="Kitchen", @@ -563,6 +567,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, name="Kitchen", @@ -750,6 +755,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -772,6 +778,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -797,6 +804,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -818,6 +826,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -853,6 +862,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -874,6 +884,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", @@ -911,6 +922,7 @@ async def test_zeroconf_pair_additionally_found_protocols( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_airplay._tcp.local.", @@ -953,6 +965,7 @@ async def test_zeroconf_pair_additionally_found_protocols( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", port=None, type="_mediaremotetv._tcp.local.", diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index a6e24b0946215..7fe2681bafd97 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -14,7 +14,7 @@ DOMAIN, ) from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, CONF_MODE, @@ -80,62 +80,6 @@ async def test_user(hass, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass, connect): - """Test import step.""" - with patch( - "homeassistant.components.asuswrt.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", - return_value=IP_ADDRESS, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_DATA, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == CONFIG_DATA - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_ssh(hass, connect): - """Test import step with ssh file.""" - config_data = CONFIG_DATA.copy() - config_data.pop(CONF_PASSWORD) - config_data[CONF_SSH_KEY] = SSH_KEY - - with patch( - "homeassistant.components.asuswrt.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", - return_value=IP_ADDRESS, - ), patch( - "homeassistant.components.asuswrt.config_flow.os.path.isfile", - return_value=True, - ), patch( - "homeassistant.components.asuswrt.config_flow.os.access", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config_data, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == config_data - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_error_no_password_ssh(hass): """Test we abort if component is already setup.""" config_data = CONFIG_DATA.copy() @@ -215,15 +159,6 @@ async def test_abort_if_already_setup(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" - # Should fail, same HOST (import) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" - async def test_on_connect_failed(hass): """Test when we have errors connecting the router.""" diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index ba6bc892e40c2..8fb7730a4e4a9 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -6,6 +6,7 @@ ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_PRESET_MODE, + DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_HEAT, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -49,7 +50,7 @@ async def test_setting_climate( await init_integration(hass, aioclient_mock) with patch("pyatag.entities.Climate.set_temp") as mock_set_temp: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_TEMPERATURE: 15}, blocking=True, @@ -59,7 +60,7 @@ async def test_setting_climate( with patch("pyatag.entities.Climate.set_preset_mode") as mock_set_preset: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -69,7 +70,7 @@ async def test_setting_climate( with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, blocking=True, diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index df83fa6d40be6..3372e8c69fa5d 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -2,7 +2,10 @@ from unittest.mock import patch from homeassistant.components.atag import DOMAIN -from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE +from homeassistant.components.water_heater import ( + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -33,7 +36,7 @@ async def test_setting_target_temperature( await init_integration(hass, aioclient_mock) with patch("pyatag.entities.DHW.set_temp") as mock_set_temp: await hass.services.async_call( - Platform.WATER_HEATER, + WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: WATER_HEATER_ID, ATTR_TEMPERATURE: 50}, blocking=True, diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 2d572b886f3e3..e419488beccbf 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -75,7 +75,16 @@ async def _mock_setup_august( return entry -async def _create_august_with_devices( # noqa: C901 +async def _create_august_with_devices( + hass, devices, api_call_side_effects=None, activities=None, pubnub=None +): + entry, api_instance = await _create_august_api_with_devices( + hass, devices, api_call_side_effects, activities, pubnub + ) + return entry + + +async def _create_august_api_with_devices( # noqa: C901 hass, devices, api_call_side_effects=None, activities=None, pubnub=None ): if api_call_side_effects is None: @@ -171,7 +180,7 @@ def unlock_return_activities_side_effect(access_token, device_id): # are any locks assert api_instance.async_status_async.mock_calls - return entry + return entry, api_instance async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub): diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py new file mode 100644 index 0000000000000..2d3e6caf884a4 --- /dev/null +++ b/tests/components/august/test_button.py @@ -0,0 +1,27 @@ +"""The button tests for the august platform.""" + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID + +from tests.components.august.mocks import ( + _create_august_api_with_devices, + _mock_lock_from_fixture, +) + + +async def test_wake_lock(hass): + """Test creation of a lock and wake it.""" + lock_one = await _mock_lock_from_fixture( + hass, "get_lock.online_with_doorsense.json" + ) + _, api_instance = await _create_august_api_with_devices(hass, [lock_one]) + entity_id = "button.online_with_doorsense_name_wake" + binary_sensor_online_with_doorsense_name = hass.states.get(entity_id) + assert binary_sensor_online_with_doorsense_name is not None + api_instance.async_status_async.reset_mock() + assert await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + await hass.async_block_till_done() + api_instance.async_status_async.assert_called_once() diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index abb4bce042d35..abb99355ef3f6 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -5,7 +5,7 @@ CONF_SERVICES, DOMAIN as AUSSIE_BROADBAND_DOMAIN, ) -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -22,6 +22,12 @@ "type": "PhoneMobile", "name": "Mobile", }, + { + "service_id": "23456789", + "description": "Fake ABB VOIP Service", + "type": "VOIP", + "name": "VOIP", + }, ] FAKE_DATA = { @@ -30,12 +36,16 @@ } -async def setup_platform(hass, platforms=[], side_effect=None, usage={}): +async def setup_platform( + hass, platforms=[], side_effect=None, usage={}, usage_effect=None +): """Set up the Aussie Broadband platform.""" mock_entry = MockConfigEntry( domain=AUSSIE_BROADBAND_DOMAIN, data=FAKE_DATA, - options={CONF_SERVICES: ["12345678", "87654321"], CONF_SCAN_INTERVAL: 30}, + options={ + CONF_SERVICES: ["12345678", "87654321", "23456789", "98765432"], + }, ) mock_entry.add_to_hass(hass) @@ -50,7 +60,9 @@ async def setup_platform(hass, platforms=[], side_effect=None, usage={}): return_value=FAKE_SERVICES, side_effect=side_effect, ), patch( - "aussiebb.asyncio.AussieBB.get_usage", return_value=usage + "aussiebb.asyncio.AussieBB.get_usage", + return_value=usage, + side_effect=usage_effect, ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index 7e919636b09b2..8a5f6b6763f83 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -4,8 +4,8 @@ from aiohttp import ClientConnectionError from aussiebb.asyncio import AuthenticationException -from homeassistant import config_entries, setup -from homeassistant.components.aussie_broadband.const import CONF_SERVICES, DOMAIN +from homeassistant import config_entries +from homeassistant.components.aussie_broadband.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -14,7 +14,7 @@ RESULT_TYPE_FORM, ) -from .common import FAKE_DATA, FAKE_SERVICES, setup_platform +from .common import FAKE_DATA, FAKE_SERVICES TEST_USERNAME = FAKE_DATA[CONF_USERNAME] TEST_PASSWORD = FAKE_DATA[CONF_PASSWORD] @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( "aussiebb.asyncio.AussieBB.login", return_value=True ), patch( - "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + "aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES ), patch( "homeassistant.components.aussie_broadband.async_setup_entry", return_value=True, @@ -45,7 +45,6 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA - assert result2["options"] == {CONF_SERVICES: ["12345678"]} assert len(mock_setup_entry.mock_calls) == 1 @@ -117,46 +116,6 @@ async def test_no_services(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_multiple_services(hass: HomeAssistant) -> None: - """Test the config flow with multiple services.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FAKE_DATA, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "service" - assert result2["errors"] is None - - with patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_SERVICES: [FAKE_SERVICES[1]["service_id"]]}, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == TEST_USERNAME - assert result3["data"] == FAKE_DATA - assert result3["options"] == { - CONF_SERVICES: [FAKE_SERVICES[1]["service_id"]], - } - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test invalid auth is handled.""" result1 = await hass.config_entries.flow.async_init( @@ -196,8 +155,6 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: async def test_reauth(hass: HomeAssistant) -> None: """Test reauth flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - # Test reauth but the entry doesn't exist result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=FAKE_DATA @@ -229,7 +186,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_REAUTH}, data=FAKE_DATA, ) - assert result5["step_id"] == "reauth" + assert result5["step_id"] == "reauth_confirm" with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() @@ -243,7 +200,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result6["step_id"] == "reauth" + assert result6["step_id"] == "reauth_confirm" # Test successful reauth with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -260,63 +217,3 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result7["type"] == "abort" assert result7["reason"] == "reauth_successful" - - -async def test_options_flow(hass): - """Test options flow.""" - entry = await setup_platform(hass) - - with patch("aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES): - - result1 = await hass.config_entries.options.async_init(entry.entry_id) - assert result1["type"] == RESULT_TYPE_FORM - assert result1["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result1["flow_id"], - user_input={CONF_SERVICES: []}, - ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert entry.options == {CONF_SERVICES: []} - - -async def test_options_flow_auth_failure(hass): - """Test options flow with auth failure.""" - - entry = await setup_platform(hass) - - with patch( - "aussiebb.asyncio.AussieBB.get_services", side_effect=AuthenticationException() - ): - - result1 = await hass.config_entries.options.async_init(entry.entry_id) - assert result1["type"] == RESULT_TYPE_ABORT - assert result1["reason"] == "invalid_auth" - - -async def test_options_flow_network_failure(hass): - """Test options flow with connectivity failure.""" - - entry = await setup_platform(hass) - - with patch( - "aussiebb.asyncio.AussieBB.get_services", side_effect=ClientConnectionError() - ): - - result1 = await hass.config_entries.options.async_init(entry.entry_id) - assert result1["type"] == RESULT_TYPE_ABORT - assert result1["reason"] == "cannot_connect" - - -async def test_options_flow_not_loaded(hass): - """Test the options flow aborts when the entry has unloaded due to a reauth.""" - - entry = await setup_platform(hass) - - with patch( - "aussiebb.asyncio.AussieBB.get_services", side_effect=AuthenticationException() - ): - entry.state = config_entries.ConfigEntryState.NOT_LOADED - result1 = await hass.config_entries.options.async_init(entry.entry_id) - assert result1["type"] == RESULT_TYPE_ABORT - assert result1["reason"] == "unknown" diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 9e31aa9b737d8..9e2e0b7cccc06 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError -from aussiebb.asyncio import AuthenticationException +from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant import data_entry_flow from homeassistant.config_entries import ConfigEntryState @@ -33,3 +33,9 @@ async def test_net_failure(hass: HomeAssistant) -> None: """Test init with a network failure.""" entry = await setup_platform(hass, side_effect=ClientConnectionError()) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_service_failure(hass: HomeAssistant) -> None: + """Test init with a invalid service.""" + entry = await setup_platform(hass, usage_effect=UnrecognisedServiceType()) + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py index 30fac808a2780..c99c52d5c86ce 100644 --- a/tests/components/aussie_broadband/test_sensor.py +++ b/tests/components/aussie_broadband/test_sensor.py @@ -1,5 +1,6 @@ """Aussie Broadband sensor platform tests.""" from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNKNOWN from .common import setup_platform @@ -24,6 +25,19 @@ "historical": [], } +MOCK_VOIP_USAGE = { + "national": {"calls": 1, "cost": 0}, + "mobile": {"calls": 2, "cost": 0}, + "international": {"calls": 3, "cost": 0}, + "sms": {}, + "internet": {}, + "voicemail": {"calls": 6, "cost": 0}, + "other": {"calls": 7, "cost": 0}, + "daysTotal": 31, + "daysRemaining": 30, + "historical": [], +} + async def test_nbn_sensor_states(hass): """Tests that the sensors are correct.""" @@ -48,3 +62,13 @@ async def test_phone_sensor_states(hass): assert hass.states.get("sensor.mobile_data_used").state == "512" assert hass.states.get("sensor.mobile_billing_cycle_length").state == "31" assert hass.states.get("sensor.mobile_billing_cycle_remaining").state == "30" + + +async def test_voip_sensor_states(hass): + """Tests that the sensors are correct.""" + + await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_VOIP_USAGE) + + assert hass.states.get("sensor.mobile_national_calls").state == "1" + assert hass.states.get("sensor.mobile_sms_sent").state == STATE_UNKNOWN + assert hass.states.get("sensor.mobile_data_used").state == STATE_UNKNOWN diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index bc03cb99f07b2..112fa57fa6461 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -298,6 +298,7 @@ async def test_reauth_flow_update_configuration(hass): SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host=DEFAULT_HOST, + addresses=[DEFAULT_HOST], port=80, hostname=f"axis-{MAC.lower()}.local.", type="_axis-video._tcp.local.", @@ -376,6 +377,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host=DEFAULT_HOST, + addresses=[DEFAULT_HOST], hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", port=80, @@ -430,6 +432,7 @@ async def test_discovered_device_already_configured( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="2.3.4.5", + addresses=["2.3.4.5"], hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", port=8080, @@ -504,6 +507,7 @@ async def test_discovery_flow_updated_configuration( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="", + addresses=[""], hostname="mock_hostname", name="", port=0, @@ -552,6 +556,7 @@ async def test_discovery_flow_ignore_non_axis_device( SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host="169.254.3.4", + addresses=["169.254.3.4"], hostname="mock_hostname", name=f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", port=80, diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index d43845e01adba..cca62babbb59d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -385,6 +385,7 @@ async def test_update_address(hass): AXIS_DOMAIN, data=zeroconf.ZeroconfServiceInfo( host="2.3.4.5", + addresses=["2.3.4.5"], hostname="mock_hostname", name="name", port=80, diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index ba8914c3b1d9f..497e8b36e99e7 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -118,18 +118,15 @@ def test_blueprint_validate(): is None ) - assert ( - models.Blueprint( - { - "blueprint": { - "name": "Hello", - "domain": "automation", - "homeassistant": {"min_version": "100000.0.0"}, - }, - } - ).validate() - == ["Requires at least Home Assistant 100000.0.0"] - ) + assert models.Blueprint( + { + "blueprint": { + "name": "Hello", + "domain": "automation", + "homeassistant": {"min_version": "100000.0.0"}, + }, + } + ).validate() == ["Requires at least Home Assistant 100000.0.0"] def test_blueprint_inputs(blueprint_2): diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index b36637897d8b7..5d3b357b9f74c 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -201,6 +201,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="test-host", + addresses=["test-host"], hostname="mock_hostname", name="test-bond-id.some-other-tail-info", port=None, @@ -238,6 +239,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="test-host", + addresses=["test-host"], hostname="mock_hostname", name="test-bond-id.some-other-tail-info", port=None, @@ -278,6 +280,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="test-host", + addresses=["test-host"], hostname="mock_hostname", name="test-bond-id.some-other-tail-info", port=None, @@ -318,6 +321,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="test-host", + addresses=["test-host"], hostname="mock_hostname", name="test-bond-id.some-other-tail-info", port=None, @@ -361,6 +365,7 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="updated-host", + addresses=["updated-host"], hostname="mock_hostname", name="already-registered-bond-id.some-other-tail-info", port=None, @@ -405,6 +410,7 @@ async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistan context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="updated-host", + addresses=["updated-host"], hostname="mock_hostname", name="already-registered-bond-id.some-other-tail-info", port=None, @@ -442,6 +448,7 @@ async def test_zeroconf_already_configured_no_reload_same_host( context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="stored-host", + addresses=["stored-host"], hostname="mock_hostname", name="already-registered-bond-id.some-other-tail-info", port=None, @@ -463,6 +470,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): source=config_entries.SOURCE_ZEROCONF, initial_input=zeroconf.ZeroconfServiceInfo( host="test-host", + addresses=["test-host"], hostname="mock_hostname", name="test-bond-id.some-other-tail-info", port=None, diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 065035bedbe77..e7c5e6d7d4d97 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -22,6 +22,7 @@ } DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="shc012345.local.", name="Bosch SHC [test-mac]._http._tcp.local.", port=0, @@ -533,6 +534,7 @@ async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): DOMAIN, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="notboschshc", port=None, diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 3c97f8ea47a9e..a3b291efd0021 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -4,6 +4,7 @@ from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.remote import ( + DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -59,7 +60,7 @@ async def test_remote_send_command(hass): remote = remotes[0] await hass.services.async_call( - Platform.REMOTE, + REMOTE_DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, @@ -86,7 +87,7 @@ async def test_remote_turn_off_turn_on(hass): remote = remotes[0] await hass.services.async_call( - Platform.REMOTE, + REMOTE_DOMAIN, SERVICE_TURN_OFF, {"entity_id": remote.entity_id}, blocking=True, @@ -94,7 +95,7 @@ async def test_remote_turn_off_turn_on(hass): assert hass.states.get(remote.entity_id).state == STATE_OFF await hass.services.async_call( - Platform.REMOTE, + REMOTE_DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, @@ -102,7 +103,7 @@ async def test_remote_turn_off_turn_on(hass): assert mock_setup.api.send_data.call_count == 0 await hass.services.async_call( - Platform.REMOTE, + REMOTE_DOMAIN, SERVICE_TURN_ON, {"entity_id": remote.entity_id}, blocking=True, @@ -110,7 +111,7 @@ async def test_remote_turn_off_turn_on(hass): assert hass.states.get(remote.entity_id).state == STATE_ON await hass.services.async_call( - Platform.REMOTE, + REMOTE_DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, blocking=True, diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3bc7738bb8df2..6dbaebdfa7bcb 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -146,6 +146,7 @@ async def test_zeroconf_snmp_error(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="example.local.", name="Brother Printer", port=None, @@ -166,6 +167,7 @@ async def test_zeroconf_unsupported_model(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="example.local.", name="Brother Printer", port=None, @@ -194,6 +196,7 @@ async def test_zeroconf_device_exists_abort(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="example.local.", name="Brother Printer", port=None, @@ -216,6 +219,7 @@ async def test_zeroconf_no_probe_existing_device(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="localhost", name="Brother Printer", port=None, @@ -242,6 +246,7 @@ async def test_zeroconf_confirm_create_entry(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="example.local.", name="Brother Printer", port=None, diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index bd3841cc4e85c..ee2a3cb297419 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -8,6 +8,7 @@ from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM EMPTY_8_6_JPEG = b"empty_8_6" +WEBRTC_ANSWER = "a=sendonly" def mock_camera_prefs(hass, entity_id, prefs=None): diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py new file mode 100644 index 0000000000000..b09f7696ef274 --- /dev/null +++ b/tests/components/camera/conftest.py @@ -0,0 +1,53 @@ +"""Test helpers for camera.""" +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.components import camera +from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC +from homeassistant.setup import async_setup_component + +from .common import WEBRTC_ANSWER + + +@pytest.fixture(name="mock_camera") +async def mock_camera_fixture(hass): + """Initialize a demo camera platform.""" + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.demo.camera.Path.read_bytes", + return_value=b"Test", + ): + yield + + +@pytest.fixture(name="mock_camera_hls") +async def mock_camera_hls_fixture(mock_camera): + """Initialize a demo camera platform with HLS.""" + with patch( + "homeassistant.components.camera.Camera.frontend_stream_type", + new_callable=PropertyMock(return_value=STREAM_TYPE_HLS), + ): + yield + + +@pytest.fixture(name="mock_camera_web_rtc") +async def mock_camera_web_rtc_fixture(hass): + """Initialize a demo camera platform with WebRTC.""" + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.camera.Camera.frontend_stream_type", + new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC), + ), patch( + "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", + return_value=WEBRTC_ANSWER, + ): + yield diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 403cacec1f104..0e53e16340498 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -8,11 +8,7 @@ import pytest from homeassistant.components import camera -from homeassistant.components.camera.const import ( - DOMAIN, - PREF_PRELOAD_STREAM, - STREAM_TYPE_WEB_RTC, -) +from homeassistant.components.camera.const import DOMAIN, 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 @@ -24,47 +20,11 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg - -from tests.components.camera import common +from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_camera_prefs, mock_turbo_jpeg STREAM_SOURCE = "rtsp://127.0.0.1/stream" HLS_STREAM_SOURCE = "http://127.0.0.1/example.m3u" WEBRTC_OFFER = "v=0\r\n" -WEBRTC_ANSWER = "a=sendonly" - - -@pytest.fixture(name="mock_camera") -async def mock_camera_fixture(hass): - """Initialize a demo camera platform.""" - assert await async_setup_component( - hass, "camera", {camera.DOMAIN: {"platform": "demo"}} - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.demo.camera.Path.read_bytes", - return_value=b"Test", - ): - yield - - -@pytest.fixture(name="mock_camera_web_rtc") -async def mock_camera_web_rtc_fixture(hass): - """Initialize a demo camera platform.""" - assert await async_setup_component( - hass, "camera", {camera.DOMAIN: {"platform": "demo"}} - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.camera.Camera.frontend_stream_type", - new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC), - ), patch( - "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", - return_value=WEBRTC_ANSWER, - ): - yield @pytest.fixture(name="mock_stream") @@ -78,7 +38,7 @@ def mock_stream_fixture(hass): @pytest.fixture(name="setup_camera_prefs") def setup_camera_prefs_fixture(hass): """Initialize HTTP API.""" - return common.mock_camera_prefs(hass, "camera.demo_camera") + return mock_camera_prefs(hass, "camera.demo_camera") @pytest.fixture(name="image_mock_url") diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py new file mode 100644 index 0000000000000..b9fb22c9ed850 --- /dev/null +++ b/tests/components/camera/test_media_source.py @@ -0,0 +1,104 @@ +"""Test camera media source.""" +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.components import media_source +from homeassistant.components.camera.const import STREAM_TYPE_WEB_RTC +from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +async def setup_media_source(hass): + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + +async def test_browsing_hls(hass, mock_camera_hls): + """Test browsing camera media source.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 0 + assert item.not_shown == 2 + + # Adding stream enables HLS camera + hass.config.components.add("stream") + + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 0 + assert len(item.children) == 2 + assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] + + +async def test_browsing_mjpeg(hass, mock_camera): + """Test browsing camera media source.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 2 + assert item.not_shown == 0 + assert item.children[0].media_content_type == "image/jpg" + assert item.children[1].media_content_type == "image/png" + + +async def test_browsing_filter_web_rtc(hass, mock_camera_web_rtc): + """Test browsing camera media source hides non-HLS cameras.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 0 + assert item.not_shown == 2 + + +async def test_resolving(hass, mock_camera_hls): + """Test resolving.""" + # Adding stream enables HLS camera + hass.config.components.add("stream") + + with patch( + "homeassistant.components.camera.media_source._async_stream_endpoint_url", + return_value="http://example.com/stream", + ): + item = await media_source.async_resolve_media( + hass, "media-source://camera/camera.demo_camera" + ) + assert item is not None + assert item.url == "http://example.com/stream" + assert item.mime_type == FORMAT_CONTENT_TYPE["hls"] + + +async def test_resolving_errors(hass, mock_camera_hls): + """Test resolving.""" + + with pytest.raises(media_source.Unresolvable) as exc_info: + await media_source.async_resolve_media( + hass, "media-source://camera/camera.demo_camera" + ) + assert str(exc_info.value) == "Stream integration not loaded" + + hass.config.components.add("stream") + + with pytest.raises(media_source.Unresolvable) as exc_info: + await media_source.async_resolve_media( + hass, "media-source://camera/camera.non_existing" + ) + assert str(exc_info.value) == "Could not resolve media item: camera.non_existing" + + with pytest.raises(media_source.Unresolvable) as exc_info, patch( + "homeassistant.components.camera.Camera.frontend_stream_type", + new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC), + ): + await media_source.async_resolve_media( + hass, "media-source://camera/camera.demo_camera" + ) + assert str(exc_info.value) == "Camera does not support MJPEG or HLS streaming." + + with pytest.raises(media_source.Unresolvable) as exc_info: + await media_source.async_resolve_media( + hass, "media-source://camera/camera.demo_camera" + ) + assert ( + str(exc_info.value) == "camera.demo_camera does not support play stream service" + ) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 67b5454b6e139..a799a6d1d3634 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -2,21 +2,34 @@ from unittest.mock import patch +import pytest + from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config +from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry, async_mock_signal async def test_service_show_view(hass, mock_zeroconf): - """Test we don't set app id in prod.""" + """Test showing a view.""" + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + # No valid URL + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "cast", + "show_lovelace_view", + {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, + blocking=True, + ) + + # Set valid URL await async_process_ha_core_config( hass, {"external_url": "https://example.com"}, ) - await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) - calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) - await hass.services.async_call( "cast", "show_lovelace_view", diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 51fe4a086a610..40a1269557d36 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access from __future__ import annotations +import asyncio import json from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import UUID @@ -10,6 +11,7 @@ import pychromecast from pychromecast.const import CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP import pytest +import yarl from homeassistant.components import media_player, tts from homeassistant.components.cast import media_player as cast @@ -37,7 +39,7 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er, network from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -468,10 +470,19 @@ async def test_discover_dynamic_group( hass ) + tasks = [] + real_create_task = asyncio.create_task + + def create_task(*args, **kwargs): + tasks.append(real_create_task(*args, **kwargs)) + # Discover cast service with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, + ), patch( + "homeassistant.components.cast.media_player.asyncio.create_task", + wraps=create_task, ): discover_cast( pychromecast.discovery.ServiceInfo( @@ -481,6 +492,10 @@ async def test_discover_dynamic_group( ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs + + assert len(tasks) == 1 + await asyncio.gather(*tasks) + tasks.clear() get_chromecast_mock.assert_called() get_chromecast_mock.reset_mock() assert add_dev1.call_count == 0 @@ -490,6 +505,9 @@ async def test_discover_dynamic_group( with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_2, + ), patch( + "homeassistant.components.cast.media_player.asyncio.create_task", + wraps=create_task, ): discover_cast( pychromecast.discovery.ServiceInfo( @@ -499,6 +517,10 @@ async def test_discover_dynamic_group( ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs + + assert len(tasks) == 1 + await asyncio.gather(*tasks) + tasks.clear() get_chromecast_mock.assert_called() get_chromecast_mock.reset_mock() assert add_dev1.call_count == 0 @@ -508,6 +530,9 @@ async def test_discover_dynamic_group( with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, + ), patch( + "homeassistant.components.cast.media_player.asyncio.create_task", + wraps=create_task, ): discover_cast( pychromecast.discovery.ServiceInfo( @@ -517,6 +542,8 @@ async def test_discover_dynamic_group( ) await hass.async_block_till_done() await hass.async_block_till_done() # having tasks that add jobs + + assert len(tasks) == 0 get_chromecast_mock.assert_not_called() assert add_dev1.call_count == 0 assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None @@ -605,6 +632,45 @@ async def test_entity_availability(hass: HomeAssistant): assert state.state == "unavailable" +@pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) +async def test_device_registry(hass: HomeAssistant, port, entry_type): + """Test device registry integration.""" + entity_id = "media_player.speaker" + reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + info = get_fake_chromecast_info(port=port) + + chromecast, _ = await async_setup_media_player_cast(hass, info) + chromecast.cast_type = pychromecast.const.CAST_TYPE_CHROMECAST + _, conn_status_cb, _ = get_status_callbacks(chromecast) + cast_entry = hass.config_entries.async_entries("cast")[0] + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "off" + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + entity_entry = reg.async_get(entity_id) + assert entity_entry.device_id is not None + device_entry = dev_reg.async_get(entity_entry.device_id) + assert device_entry.entry_type == entry_type + + # Check that the chromecast object is torn down when the device is removed + chromecast.disconnect.assert_not_called() + dev_reg.async_update_device( + device_entry.id, remove_config_entry_id=cast_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + chromecast.disconnect.assert_called_once() + + async def test_entity_cast_status(hass: HomeAssistant): """Test handling of cast status.""" entity_id = "media_player.speaker" @@ -790,8 +856,8 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 in response["result"]["children"] @@ -802,8 +868,8 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "media_content_id": "media-source://media_source/local/test.mp3", "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -846,8 +912,8 @@ async def test_entity_browse_media_audio_only( "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 not in response["result"]["children"] @@ -858,8 +924,8 @@ async def test_entity_browse_media_audio_only( "media_content_id": "media-source://media_source/local/test.mp3", "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -1001,7 +1067,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): await async_process_ha_core_config( hass, - {"external_url": "http://example.com:8123"}, + {"internal_url": "http://example.com:8123"}, ) info = get_fake_chromecast_info() @@ -1119,7 +1185,7 @@ async def test_entity_control(hass: HomeAssistant): # Turn on await common.async_turn_on(hass, entity_id) chromecast.play_media.assert_called_once_with( - "https://www.home-assistant.io/images/cast/splash.png", ANY + "https://www.home-assistant.io/images/cast/splash.png", "image/png" ) chromecast.quit_app.reset_mock() @@ -1795,8 +1861,8 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): "media_content_id": "", "can_play": False, "can_expand": True, - "children_media_class": None, "thumbnail": "https://brands.home-assistant.io/_/spotify/logo.png", + "children_media_class": None, } assert expected_child in response["result"]["children"] @@ -1822,5 +1888,72 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): "children_media_class": None, "thumbnail": None, "children": [], + "not_shown": 0, } assert response["result"] == expected_response + + +async def test_cast_platform_play_media_local_media( + hass: HomeAssistant, quick_play_mock, caplog +): + """Test we process data when playing local media.""" + entity_id = "media_player.speaker" + info = get_fake_chromecast_info() + + chromecast, _ = await async_setup_media_player_cast(hass, info) + _, conn_status_cb, _ = get_status_callbacks(chromecast) + + # Bring Chromecast online + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + # This will play using the cast platform + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: entity_id, + media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl", + media_player.ATTR_MEDIA_CONTENT_ID: "/api/hls/bla/master_playlist.m3u8", + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Assert we added extra play information + quick_play_mock.assert_called() + app_data = quick_play_mock.call_args[0][2] + + assert not app_data["media_id"].startswith("/") + assert "authSig" in yarl.URL(app_data["media_id"]).query + assert app_data["media_type"] == "application/vnd.apple.mpegurl" + assert app_data["stream_type"] == "LIVE" + assert app_data["media_info"] == { + "hlsVideoSegmentFormat": "fmp4", + } + + quick_play_mock.reset_mock() + + # Test not appending if we have a signature + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: entity_id, + media_player.ATTR_MEDIA_CONTENT_TYPE: "application/vnd.apple.mpegurl", + media_player.ATTR_MEDIA_CONTENT_ID: f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla", + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Assert we added extra play information + quick_play_mock.assert_called() + app_data = quick_play_mock.call_args[0][2] + # No authSig appended + assert ( + app_data["media_id"] + == f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla" + ) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 07b87632f1907..2360526864947 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -392,6 +392,7 @@ async def test_websocket_status( "logged_in": True, "email": "hello@home-assistant.io", "cloud": "connected", + "cloud_last_disconnect_reason": None, "prefs": { "alexa_enabled": True, "cloudhooks": {}, @@ -424,6 +425,7 @@ async def test_websocket_status( "exclude_entities": [], }, "google_registered": False, + "google_local_connected": False, "remote_domain": None, "remote_connected": False, "remote_certificate": None, diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 4a513aff1172d..78a8f83eef689 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -137,6 +137,14 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(hass.states.async_entity_ids("binary_sensor")) == 0 + cloud_states = [] + + def handle_state(cloud_state): + nonlocal cloud_states + cloud_states.append(cloud_state) + + cloud.async_listen_connection_change(hass, handle_state) + assert "async_setup" in str(cl.iot._on_connect[-1]) await cl.iot._on_connect[-1]() await hass.async_block_till_done() @@ -149,6 +157,17 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(mock_load.mock_calls) == 0 + assert len(cloud_states) == 1 + assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + + assert len(cl.iot._on_disconnect) == 2 + assert "async_setup" in str(cl.iot._on_disconnect[-1]) + await cl.iot._on_disconnect[-1]() + await hass.async_block_till_done() + + assert len(cloud_states) == 2 + assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + async def test_remote_ui_url(hass, mock_cloud_fixture): """Test getting remote ui url.""" diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 532e14573f405..b523b02aa64bd 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -51,6 +51,7 @@ async def test_template(hass: HomeAssistant) -> None: ) entity_state = hass.states.get("binary_sensor.test") + assert entity_state assert entity_state.state == STATE_ON @@ -65,10 +66,11 @@ async def test_sensor_off(hass: HomeAssistant) -> None: }, ) entity_state = hass.states.get("binary_sensor.test") + assert entity_state assert entity_state.state == STATE_OFF -async def test_unique_id(hass): +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option and if it only creates one binary sensor per id.""" assert await setup.async_setup_component( hass, diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 9d4f5b60c8bbb..98c917f51ba76 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -6,6 +6,8 @@ from typing import Any from unittest.mock import patch +from pytest import LogCaptureFixture + from homeassistant import config as hass_config, setup from homeassistant.components.cover import DOMAIN, SCAN_INTERVAL from homeassistant.const import ( @@ -36,7 +38,7 @@ async def setup_test_entity(hass: HomeAssistant, config_dict: dict[str, Any]) -> await hass.async_block_till_done() -async def test_no_covers(caplog: Any, hass: HomeAssistant) -> None: +async def test_no_covers(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" with patch( @@ -150,7 +152,9 @@ async def test_reload(hass: HomeAssistant) -> None: assert hass.states.get("cover.from_yaml") -async def test_move_cover_failure(caplog: Any, hass: HomeAssistant) -> None: +async def test_move_cover_failure( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test with state value.""" await setup_test_entity( @@ -163,7 +167,7 @@ async def test_move_cover_failure(caplog: Any, hass: HomeAssistant) -> None: assert "Command failed" in caplog.text -async def test_unique_id(hass): +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option and if it only creates one cover per id.""" await setup_test_entity( hass, diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 561ac07df20ce..26b53a827e76d 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -7,6 +7,8 @@ from typing import Any from unittest.mock import patch +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.notify import DOMAIN from homeassistant.core import HomeAssistant @@ -60,7 +62,9 @@ async def test_command_line_output(hass: HomeAssistant) -> None: assert message == handle.read() -async def test_error_for_none_zero_exit_code(caplog: Any, hass: HomeAssistant) -> None: +async def test_error_for_none_zero_exit_code( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test if an error is logged for non zero exit codes.""" await setup_test_service( hass, @@ -75,7 +79,7 @@ async def test_error_for_none_zero_exit_code(caplog: Any, hass: HomeAssistant) - assert "Command failed" in caplog.text -async def test_timeout(caplog: Any, hass: HomeAssistant) -> None: +async def test_timeout(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: """Test blocking is not forever.""" await setup_test_service( hass, @@ -90,7 +94,9 @@ async def test_timeout(caplog: Any, hass: HomeAssistant) -> None: assert "Timeout" in caplog.text -async def test_subprocess_exceptions(caplog: Any, hass: HomeAssistant) -> None: +async def test_subprocess_exceptions( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test that notify subprocess exceptions are handled correctly.""" with patch( diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index c9a4860b987d1..62ec1dbe97bd0 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -4,6 +4,8 @@ from typing import Any from unittest.mock import patch +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.sensor import DOMAIN from homeassistant.core import HomeAssistant @@ -98,7 +100,9 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None: ) -async def test_bad_template_render(caplog: Any, hass: HomeAssistant) -> None: +async def test_bad_template_render( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test rendering a broken template.""" await setup_test_entities( @@ -141,7 +145,9 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: assert entity_state.attributes["key_three"] == "value_three" -async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_no_data( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test attributes when no JSON result fetched.""" await setup_test_entities( @@ -157,7 +163,9 @@ async def test_update_with_json_attrs_no_data(caplog, hass: HomeAssistant) -> No assert "Empty reply found when expecting JSON data" in caplog.text -async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_not_dict( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test attributes when the return value not a dict.""" await setup_test_entities( @@ -173,7 +181,9 @@ async def test_update_with_json_attrs_not_dict(caplog, hass: HomeAssistant) -> N assert "JSON result was not a dictionary" in caplog.text -async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_update_with_json_attrs_bad_json( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test attributes when the return value is invalid JSON.""" await setup_test_entities( @@ -189,7 +199,9 @@ async def test_update_with_json_attrs_bad_json(caplog, hass: HomeAssistant) -> N assert "Unable to parse output as JSON" in caplog.text -async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_update_with_missing_json_attrs( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test attributes when an expected key is missing.""" await setup_test_entities( @@ -208,7 +220,9 @@ async def test_update_with_missing_json_attrs(caplog, hass: HomeAssistant) -> No assert "missing_key" not in entity_state.attributes -async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistant) -> None: # type: ignore[no-untyped-def] +async def test_update_with_unnecessary_json_attrs( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test attributes when an expected key is missing.""" await setup_test_entities( @@ -226,7 +240,7 @@ async def test_update_with_unnecessary_json_attrs(caplog, hass: HomeAssistant) - assert "key_three" not in entity_state.attributes -async def test_unique_id(hass): +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option and if it only creates one sensor per id.""" assert await setup.async_setup_component( hass, diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index f918c7500ad66..307974ab3fedf 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -8,6 +8,8 @@ from typing import Any from unittest.mock import patch +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.switch import DOMAIN, SCAN_INTERVAL from homeassistant.const import ( @@ -269,10 +271,13 @@ async def test_name_is_set_correctly(hass: HomeAssistant) -> None: ) entity_state = hass.states.get("switch.test") + assert entity_state assert entity_state.name == "Test friendly name!" -async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistant) -> None: +async def test_switch_command_state_fail( + caplog: LogCaptureFixture, hass: HomeAssistant +) -> None: """Test that switch failures are handled correctly.""" await setup_test_entity( hass, @@ -289,6 +294,7 @@ async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistant) -> No await hass.async_block_till_done() entity_state = hass.states.get("switch.test") + assert entity_state assert entity_state.state == "on" await hass.services.async_call( @@ -300,13 +306,14 @@ async def test_switch_command_state_fail(caplog: Any, hass: HomeAssistant) -> No await hass.async_block_till_done() entity_state = hass.states.get("switch.test") + assert entity_state assert entity_state.state == "on" assert "Command failed" in caplog.text async def test_switch_command_state_code_exceptions( - caplog: Any, hass: HomeAssistant + caplog: LogCaptureFixture, hass: HomeAssistant ) -> None: """Test that switch state code exceptions are handled correctly.""" @@ -339,7 +346,7 @@ async def test_switch_command_state_code_exceptions( async def test_switch_command_state_value_exceptions( - caplog: Any, hass: HomeAssistant + caplog: LogCaptureFixture, hass: HomeAssistant ) -> None: """Test that switch state value exceptions are handled correctly.""" @@ -372,14 +379,14 @@ async def test_switch_command_state_value_exceptions( assert "Error trying to exec command" in caplog.text -async def test_no_switches(caplog: Any, hass: HomeAssistant) -> None: +async def test_no_switches(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: """Test with no switches.""" await setup_test_entity(hass, {}) assert "No switches" in caplog.text -async def test_unique_id(hass): +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option and if it only creates one switch per id.""" await setup_test_entity( hass, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6608bf3471dc9..06b9f1ae7f6c3 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -9,10 +9,10 @@ from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.components.config import config_entries -from homeassistant.config_entries import HANDLERS +from homeassistant.config_entries import HANDLERS, ConfigFlow from homeassistant.core import callback from homeassistant.generated import config_flows -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.setup import async_setup_component from tests.common import ( @@ -94,6 +94,7 @@ def async_supports_options_flow(cls, config_entry): "source": "bla", "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, + "supports_remove_device": False, "supports_unload": True, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -106,6 +107,7 @@ def async_supports_options_flow(cls, config_entry): "source": "bla2", "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, + "supports_remove_device": False, "supports_unload": False, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -118,6 +120,7 @@ def async_supports_options_flow(cls, config_entry): "source": "bla3", "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, + "supports_remove_device": False, "supports_unload": False, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -190,6 +193,37 @@ async def test_reload_entry_in_failed_state(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 +async def test_reload_entry_in_setup_retry(hass, client, hass_admin_user): + """Test reloading an entry via the API that is in setup retry.""" + mock_setup_entry = AsyncMock(return_value=True) + mock_unload_entry = AsyncMock(return_value=True) + mock_migrate_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_migrate_entry=mock_migrate_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + entry = MockConfigEntry(domain="comp", state=core_ce.ConfigEntryState.SETUP_RETRY) + entry.supports_unload = True + entry.add_to_hass(hass) + + with patch.dict(HANDLERS, {"comp": ConfigFlow, "test": ConfigFlow}): + resp = await client.post( + f"/api/config/config_entries/entry/{entry.entry_id}/reload" + ) + await hass.async_block_till_done() + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == {"require_restart": False} + assert len(hass.config_entries.async_entries()) == 1 + + async def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(config_flows, "FLOWS", ["hello", "world"]): @@ -370,6 +404,7 @@ async def async_step_user(self, user_input=None): "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, + "supports_remove_device": False, "supports_unload": False, "pref_disable_new_entities": False, "pref_disable_polling": False, @@ -443,6 +478,7 @@ async def async_step_account(self, user_input=None): "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, + "supports_remove_device": False, "supports_unload": False, "pref_disable_new_entities": False, "pref_disable_polling": False, diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 11b2f663f19b9..f923b32610098 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -3,8 +3,14 @@ from homeassistant.components.config import device_registry from homeassistant.helpers import device_registry as helpers_dr +from homeassistant.setup import async_setup_component -from tests.common import mock_device_registry +from tests.common import ( + MockConfigEntry, + MockModule, + mock_device_registry, + mock_integration, +) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -80,7 +86,19 @@ async def test_list_devices(hass, client, registry): ] -async def test_update_device(hass, client, registry): +@pytest.mark.parametrize( + "payload_key,payload_value", + [ + ["area_id", "12345A"], + ["area_id", None], + ["disabled_by", helpers_dr.DeviceEntryDisabler.USER], + ["disabled_by", "user"], + ["disabled_by", None], + ["name_by_user", "Test Friendly Name"], + ["name_by_user", None], + ], +) +async def test_update_device(hass, client, registry, payload_key, payload_value): """Test update entry.""" device = registry.async_get_or_create( config_entry_id="1234", @@ -90,24 +108,292 @@ async def test_update_device(hass, client, registry): model="model", ) - assert not device.area_id - assert not device.name_by_user + assert not getattr(device, payload_key) await client.send_json( { "id": 1, - "device_id": device.id, - "area_id": "12345A", - "name_by_user": "Test Friendly Name", - "disabled_by": helpers_dr.DeviceEntryDisabler.USER, "type": "config/device_registry/update", + "device_id": device.id, + payload_key: payload_value, } ) msg = await client.receive_json() - - assert msg["result"]["id"] == device.id - assert msg["result"]["area_id"] == "12345A" - assert msg["result"]["name_by_user"] == "Test Friendly Name" - assert msg["result"]["disabled_by"] == helpers_dr.DeviceEntryDisabler.USER + await hass.async_block_till_done() assert len(registry.devices) == 1 + + device = registry.async_get_device( + identifiers={("bridgeid", "0123")}, + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + ) + + assert msg["result"][payload_key] == payload_value + assert getattr(device, payload_key) == payload_value + + assert isinstance(device.disabled_by, (helpers_dr.DeviceEntryDisabler, type(None))) + + +async def test_remove_config_entry_from_device(hass, hass_ws_client): + """Test removing config entry from device.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + device_registry = mock_device_registry(hass) + + can_remove = False + + async def async_remove_config_entry_device(hass, config_entry, device_entry): + return can_remove + + mock_integration( + hass, + MockModule( + "comp1", async_remove_config_entry_device=async_remove_config_entry_device + ), + ) + mock_integration( + hass, + MockModule( + "comp2", async_remove_config_entry_device=async_remove_config_entry_device + ), + ) + + entry_1 = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry_1.supports_remove_device = True + entry_1.add_to_hass(hass) + + entry_2 = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry_2.supports_remove_device = True + entry_2.add_to_hass(hass) + + device_registry.async_get_or_create( + config_entry_id=entry_1.entry_id, + connections={(helpers_dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=entry_2.entry_id, + connections={(helpers_dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == {entry_1.entry_id, entry_2.entry_id} + + # Try removing a config entry from the device, it should fail because + # async_remove_config_entry_device returns False + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_1.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + + # Make async_remove_config_entry_device return True + can_remove = True + + # Remove the 1st config entry + await ws_client.send_json( + { + "id": 6, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_1.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"]["config_entries"] == [entry_2.entry_id] + + # Check that the config entry was removed from the device + assert device_registry.async_get(device_entry.id).config_entries == { + entry_2.entry_id + } + + # Remove the 2nd config entry + await ws_client.send_json( + { + "id": 7, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_2.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] is None + + # This was the last config entry, the device is removed + assert not device_registry.async_get(device_entry.id) + + +async def test_remove_config_entry_from_device_fails(hass, hass_ws_client): + """Test removing config entry from device failing cases.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + device_registry = mock_device_registry(hass) + + async def async_remove_config_entry_device(hass, config_entry, device_entry): + return True + + mock_integration( + hass, + MockModule("comp1"), + ) + mock_integration( + hass, + MockModule( + "comp2", async_remove_config_entry_device=async_remove_config_entry_device + ), + ) + + entry_1 = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry_1.add_to_hass(hass) + + entry_2 = MockConfigEntry( + domain="comp2", + title="Test 1", + source="bla", + ) + entry_2.supports_remove_device = True + entry_2.add_to_hass(hass) + + entry_3 = MockConfigEntry( + domain="comp3", + title="Test 1", + source="bla", + ) + entry_3.supports_remove_device = True + entry_3.add_to_hass(hass) + + device_registry.async_get_or_create( + config_entry_id=entry_1.entry_id, + connections={(helpers_dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_registry.async_get_or_create( + config_entry_id=entry_2.entry_id, + connections={(helpers_dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=entry_3.entry_id, + connections={(helpers_dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == { + entry_1.entry_id, + entry_2.entry_id, + entry_3.entry_id, + } + + fake_entry_id = "abc123" + assert entry_1.entry_id != fake_entry_id + fake_device_id = "abc123" + assert device_entry.id != fake_device_id + + # Try removing a non existing config entry from the device + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": fake_entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Unknown config entry" + + # Try removing a config entry which does not support removal from the device + await ws_client.send_json( + { + "id": 6, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_1.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert ( + response["error"]["message"] == "Config entry does not support device removal" + ) + + # Try removing a config entry from a device which does not exist + await ws_client.send_json( + { + "id": 7, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_2.entry_id, + "device_id": fake_device_id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Unknown device" + + # Try removing a config entry from a device which it's not connected to + await ws_client.send_json( + { + "id": 8, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_2.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert set(response["result"]["config_entries"]) == { + entry_1.entry_id, + entry_3.entry_id, + } + + await ws_client.send_json( + { + "id": 9, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_2.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Config entry not in device" + + # Try removing a config entry which can't be loaded from a device - allowed + await ws_client.send_json( + { + "id": 10, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry_3.entry_id, + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Integration not found" diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 5421f6e1d52be..3a6a56fe09787 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -124,6 +124,7 @@ async def test_api_password_abort(hass): SOURCE_ZEROCONF, zeroconf.ZeroconfServiceInfo( host=HOST, + addresses=[HOST], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 11f9483e27723..0bd308caeed78 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, @@ -10,14 +12,13 @@ DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH -from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -34,204 +35,512 @@ async def test_no_binary_sensors(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): - """Test successful creation of binary sensor entities.""" - data = { - "sensors": { - "1": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": {"on": True, "reachable": True, "temperature": 10}, - "uniqueid": "00:00:00:00:00:00:00:00-00", +TEST_DATA = [ + ( # Alarm binary sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + "temperature": 2600, }, - "2": { - "name": "Temperature sensor", - "type": "ZHATemperature", - "state": {"temperature": False}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:01-00", + "ep": 1, + "etag": "18c0f3c2100904e31a7f938db2ba9ba9", + "manufacturername": "dresden elektronik", + "modelid": "lumi.sensor_motion.aq2", + "name": "Alarm 10", + "state": { + "alarm": False, + "lastupdated": "none", + "lowbattery": None, + "tampered": None, }, - "3": { - "name": "CLIP presence sensor", - "type": "CLIPPresence", - "state": {"presence": False}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:02-00", + "swversion": "20170627", + "type": "ZHAAlarm", + "uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "binary_sensor.alarm_10", + "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.SAFETY, + "attributes": { + "on": True, + "temperature": 26.0, + "device_class": "safety", + "friendly_name": "Alarm 10", }, - "4": { - "name": "Vibration sensor", - "type": "ZHAVibration", - "state": { - "orientation": [1, 2, 3], - "tiltangle": 36, - "vibration": True, - "vibrationstrength": 10, - }, - "config": {"on": True, "reachable": True, "temperature": 10}, - "uniqueid": "00:00:00:00:00:00:00:03-00", + "websocket_event": {"alarm": True}, + "next_state": STATE_ON, + }, + ), + ( # Carbon monoxide binary sensor + { + "config": { + "battery": 100, + "on": True, + "pending": [], + "reachable": True, }, - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 5 - presence_sensor = hass.states.get("binary_sensor.presence_sensor") - assert presence_sensor.state == STATE_OFF - assert ( - presence_sensor.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION - ) - presence_temp = hass.states.get("sensor.presence_sensor_temperature") - assert presence_temp.state == "0.1" - assert presence_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE - assert hass.states.get("binary_sensor.temperature_sensor") is None - assert hass.states.get("binary_sensor.clip_presence_sensor") is None - vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") - assert vibration_sensor.state == STATE_ON - assert ( - vibration_sensor.attributes[ATTR_DEVICE_CLASS] - == BinarySensorDeviceClass.VIBRATION - ) - vibration_temp = hass.states.get("sensor.vibration_sensor_temperature") - assert vibration_temp.state == "0.1" - assert vibration_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + "ep": 1, + "etag": "b7599df551944df97b2aa87d160b9c45", + "manufacturername": "Heiman", + "modelid": "CO_V16", + "name": "Cave CO", + "state": { + "carbonmonoxide": False, + "lastupdated": "none", + "lowbattery": False, + "tampered": False, + }, + "swversion": "20150330", + "type": "ZHACarbonMonoxide", + "uniqueid": "00:15:8d:00:02:a5:21:24-01-0101", + }, + { + "entity_count": 4, + "device_count": 3, + "entity_id": "binary_sensor.cave_co", + "unique_id": "00:15:8d:00:02:a5:21:24-01-0101", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.CO, + "attributes": { + "on": True, + "device_class": "carbon_monoxide", + "friendly_name": "Cave CO", + }, + "websocket_event": {"carbonmonoxide": True}, + "next_state": STATE_ON, + }, + ), + ( # Fire binary sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "2b585d2c016bfd665ba27a8fdad28670", + "manufacturername": "LUMI", + "modelid": "lumi.sensor_smoke", + "name": "sensor_kitchen_smoke", + "state": { + "fire": False, + "lastupdated": "2018-02-20T11:25:02", + }, + "type": "ZHAFire", + "uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "binary_sensor.sensor_kitchen_smoke", + "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.SMOKE, + "attributes": { + "on": True, + "device_class": "smoke", + "friendly_name": "sensor_kitchen_smoke", + }, + "websocket_event": {"fire": True}, + "next_state": STATE_ON, + }, + ), + ( # Fire test mode binary sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "2b585d2c016bfd665ba27a8fdad28670", + "manufacturername": "LUMI", + "modelid": "lumi.sensor_smoke", + "name": "sensor_kitchen_smoke", + "state": { + "fire": False, + "test": False, + "lastupdated": "2018-02-20T11:25:02", + }, + "type": "ZHAFire", + "uniqueid": "00:15:8d:00:01:d9:3e:7c-01-0500", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "binary_sensor.sensor_kitchen_smoke_test_mode", + "unique_id": "00:15:8d:00:01:d9:3e:7c-test mode", + "state": STATE_OFF, + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": BinarySensorDeviceClass.SMOKE, + "attributes": { + "device_class": "smoke", + "friendly_name": "sensor_kitchen_smoke Test Mode", + }, + "websocket_event": {"test": True}, + "next_state": STATE_ON, + }, + ), + ( # Generic flag binary sensor + { + "config": { + "on": True, + "reachable": True, + }, + "modelid": "Switch", + "name": "Kitchen Switch", + "state": { + "flag": True, + "lastupdated": "2018-07-01T10:40:35", + }, + "swversion": "1.0.0", + "type": "CLIPGenericFlag", + "uniqueid": "kitchen-switch", + }, + { + "entity_count": 1, + "device_count": 2, + "entity_id": "binary_sensor.kitchen_switch", + "unique_id": "kitchen-switch", + "state": STATE_ON, + "entity_category": None, + "device_class": None, + "attributes": { + "on": True, + "friendly_name": "Kitchen Switch", + }, + "websocket_event": {"flag": False}, + "next_state": STATE_OFF, + }, + ), + ( # Open/Close binary sensor + { + "config": { + "battery": 95, + "on": True, + "reachable": True, + "temperature": 3300, + }, + "ep": 1, + "etag": "66cc641d0368110da6882b50090174ac", + "manufacturername": "LUMI", + "modelid": "lumi.sensor_magnet.aq2", + "name": "Back Door", + "state": { + "lastupdated": "2019-05-05T14:54:32", + "open": False, + }, + "swversion": "20161128", + "type": "ZHAOpenClose", + "uniqueid": "00:15:8d:00:02:2b:96:b4-01-0006", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "binary_sensor.back_door", + "unique_id": "00:15:8d:00:02:2b:96:b4-01-0006", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.OPENING, + "attributes": { + "on": True, + "temperature": 33.0, + "device_class": "opening", + "friendly_name": "Back Door", + }, + "websocket_event": {"open": True}, + "next_state": STATE_ON, + }, + ), + ( # Presence binary sensor + { + "config": { + "alert": "none", + "battery": 100, + "delay": 0, + "ledindication": False, + "on": True, + "pending": [], + "reachable": True, + "sensitivity": 1, + "sensitivitymax": 2, + "usertest": False, + }, + "ep": 2, + "etag": "5cfb81765e86aa53ace427cfd52c6d52", + "manufacturername": "Philips", + "modelid": "SML001", + "name": "Motion sensor 4", + "state": { + "dark": False, + "lastupdated": "2019-05-05T14:37:06", + "presence": False, + }, + "swversion": "6.1.0.18912", + "type": "ZHAPresence", + "uniqueid": "00:17:88:01:03:28:8c:9b-02-0406", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "binary_sensor.motion_sensor_4", + "unique_id": "00:17:88:01:03:28:8c:9b-02-0406", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.MOTION, + "attributes": { + "on": True, + "dark": False, + "device_class": "motion", + "friendly_name": "Motion sensor 4", + }, + "websocket_event": {"presence": True}, + "next_state": STATE_ON, + }, + ), + ( # Water leak binary sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + "temperature": 2500, + }, + "ep": 1, + "etag": "fae893708dfe9b358df59107d944fa1c", + "manufacturername": "LUMI", + "modelid": "lumi.sensor_wleak.aq1", + "name": "water2", + "state": { + "lastupdated": "2019-01-29T07:13:20", + "lowbattery": False, + "tampered": False, + "water": False, + }, + "swversion": "20170721", + "type": "ZHAWater", + "uniqueid": "00:15:8d:00:02:2f:07:db-01-0500", + }, + { + "entity_count": 5, + "device_count": 3, + "entity_id": "binary_sensor.water2", + "unique_id": "00:15:8d:00:02:2f:07:db-01-0500", + "state": STATE_OFF, + "entity_category": None, + "device_class": BinarySensorDeviceClass.MOISTURE, + "attributes": { + "on": True, + "temperature": 25.0, + "device_class": "moisture", + "friendly_name": "water2", + }, + "websocket_event": {"water": True}, + "next_state": STATE_ON, + }, + ), + ( # Vibration binary sensor + { + "config": { + "battery": 91, + "on": True, + "pending": [], + "reachable": True, + "sensitivity": 21, + "sensitivitymax": 21, + "temperature": 3200, + }, + "ep": 1, + "etag": "b7599df551944df97b2aa87d160b9c45", + "manufacturername": "LUMI", + "modelid": "lumi.vibration.aq1", + "name": "Vibration 1", + "state": { + "lastupdated": "2019-03-09T15:53:07", + "orientation": [10, 1059, 0], + "tiltangle": 83, + "vibration": True, + "vibrationstrength": 114, + }, + "swversion": "20180130", + "type": "ZHAVibration", + "uniqueid": "00:15:8d:00:02:a5:21:24-01-0101", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "binary_sensor.vibration_1", + "unique_id": "00:15:8d:00:02:a5:21:24-01-0101", + "state": STATE_ON, + "entity_category": None, + "device_class": BinarySensorDeviceClass.VIBRATION, + "attributes": { + "on": True, + "temperature": 32.0, + "orientation": [10, 1059, 0], + "tiltangle": 83, + "vibrationstrength": 114, + "device_class": "vibration", + "friendly_name": "Vibration 1", + }, + "websocket_event": {"vibration": False}, + "next_state": STATE_OFF, + }, + ), + ( # Tampering binary sensor + { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": { + "dark": False, + "lowbattery": False, + "presence": False, + "tampered": False, + }, + "config": { + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + { + "entity_count": 4, + "device_count": 3, + "entity_id": "binary_sensor.presence_sensor_tampered", + "unique_id": "00:00:00:00:00:00:00:00-tampered", + "state": STATE_OFF, + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": BinarySensorDeviceClass.TAMPER, + "attributes": { + "device_class": "tamper", + "friendly_name": "Presence sensor Tampered", + }, + "websocket_event": {"tampered": True}, + "next_state": STATE_ON, + }, + ), + ( # Low battery binary sensor + { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": { + "dark": False, + "lowbattery": False, + "presence": False, + "tampered": False, + }, + "config": { + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + { + "entity_count": 4, + "device_count": 3, + "entity_id": "binary_sensor.presence_sensor_low_battery", + "unique_id": "00:00:00:00:00:00:00:00-low battery", + "state": STATE_OFF, + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": BinarySensorDeviceClass.BATTERY, + "attributes": { + "device_class": "battery", + "friendly_name": "Presence sensor Low Battery", + }, + "websocket_event": {"lowbattery": True}, + "next_state": STATE_ON, + }, + ), +] - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "state": {"presence": True}, - } - await mock_deconz_websocket(data=event_changed_sensor) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.presence_sensor").state == STATE_ON +@pytest.mark.parametrize("sensor_data, expected", TEST_DATA) +async def test_binary_sensors( + hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected +): + """Test successful creation of binary sensor entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) - await hass.config_entries.async_unload(config_entry.entry_id) + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): + config_entry = await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} + ) - assert hass.states.get("binary_sensor.presence_sensor").state == STATE_UNAVAILABLE + assert len(hass.states.async_all()) == expected["entity_count"] - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() + # Verify state data - assert len(hass.states.async_all()) == 0 + sensor = hass.states.get(expected["entity_id"]) + assert sensor.state == expected["state"] + assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"] + assert sensor.attributes == expected["attributes"] + # Verify entity registry data -async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): - """Verify tampering sensor works.""" - data = { - "sensors": { - "1": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": { - "dark": False, - "lowbattery": False, - "presence": False, - "tampered": False, - }, - "config": {"on": True, "reachable": True, "temperature": 10}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] - ent_reg = er.async_get(hass) + # Verify device registry data - assert len(hass.states.async_all()) == 4 - hass.states.get("binary_sensor.presence_sensor_low_battery").state == STATE_OFF - assert ( - ent_reg.async_get("binary_sensor.presence_sensor_low_battery").entity_category - is EntityCategory.DIAGNOSTIC - ) - presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered") - assert presence_tamper.state == STATE_OFF assert ( - presence_tamper.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.TAMPER - ) - assert ( - ent_reg.async_get("binary_sensor.presence_sensor_tampered").entity_category - is EntityCategory.DIAGNOSTIC + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] ) + # Change state + event_changed_sensor = { "t": "event", "e": "changed", "r": "sensors", "id": "1", - "state": {"tampered": True}, + "state": expected["websocket_event"], } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() + assert hass.states.get(expected["entity_id"]).state == expected["next_state"] - assert hass.states.get("binary_sensor.presence_sensor_tampered").state == STATE_ON + # Unload entry await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - assert ( - hass.states.get("binary_sensor.presence_sensor_tampered").state - == STATE_UNAVAILABLE - ) + # Remove entry 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_fire_sensor(hass, aioclient_mock, mock_deconz_websocket): - """Verify smoke alarm sensor works.""" +async def test_not_allow_clip_sensor(hass, aioclient_mock): + """Test that CLIP sensors are not allowed.""" data = { "sensors": { "1": { - "name": "Fire alarm", - "type": "ZHAFire", - "state": {"fire": False, "test": False}, - "config": {"on": True, "reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:00-00", + "name": "CLIP presence sensor", + "type": "CLIPPresence", + "state": {"presence": False}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, } } - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - ent_reg = er.async_get(hass) - - assert len(hass.states.async_all()) == 2 - assert hass.states.get("binary_sensor.fire_alarm").state == STATE_OFF - assert ent_reg.async_get("binary_sensor.fire_alarm").entity_category is None - - assert hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_OFF - assert ( - ent_reg.async_get("binary_sensor.fire_alarm_test_mode").entity_category - is EntityCategory.DIAGNOSTIC - ) - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "state": {"fire": True, "test": True}, - } - await mock_deconz_websocket(data=event_changed_sensor) - await hass.async_block_till_done() - - assert hass.states.get("binary_sensor.fire_alarm").state == STATE_ON - assert hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_ON - - await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get("binary_sensor.fire_alarm").state == STATE_UNAVAILABLE - assert ( - hass.states.get("binary_sensor.fire_alarm_test_mode").state == STATE_UNAVAILABLE - ) + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} + ) - await hass.config_entries.async_remove(config_entry.entry_id) - await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py new file mode 100644 index 0000000000000..804a93d5ea40d --- /dev/null +++ b/tests/components/deconz/test_button.py @@ -0,0 +1,106 @@ +"""deCONZ button platform tests.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) + + +async def test_no_binary_sensors(hass, aioclient_mock): + """Test that no sensors in deconz results in no sensor entities.""" + await setup_deconz_integration(hass, aioclient_mock) + assert len(hass.states.async_all()) == 0 + + +TEST_DATA = [ + ( # Store scene button + { + "groups": { + "1": { + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene"}], + "lights": [], + } + } + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "button.light_group_scene_store_current_scene", + "unique_id": "01234E56789A/groups/1/scenes/1-store", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "icon": "mdi:inbox-arrow-down", + "friendly_name": "Light group Scene Store Current Scene", + }, + "request": "/groups/1/scenes/1/store", + }, + ), +] + + +@pytest.mark.parametrize("raw_data, expected", TEST_DATA) +async def test_button(hass, aioclient_mock, raw_data, expected): + """Test successful creation of button entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + with patch.dict(DECONZ_WEB_REQUEST, raw_data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify state data + + button = hass.states.get(expected["entity_id"]) + assert button.attributes == expected["attributes"] + + # Verify entity registry data + + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] + + # Verify device registry data + + assert ( + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] + ) + + # Verify button press + + mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: expected["entity_id"]}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {} + + # Unload entry + + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE + + # Remove entry + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 3febbae510b39..a21d981900c88 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -111,7 +111,7 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket assert climate_thermostat.attributes["current_temperature"] == 21.0 assert climate_thermostat.attributes["temperature"] == 21.0 assert climate_thermostat.attributes["locked"] is True - assert hass.states.get("sensor.thermostat_battery_level").state == "59" + assert hass.states.get("sensor.thermostat_battery").state == "59" # Event signals thermostat configured off @@ -211,7 +211,7 @@ async def test_climate_device_without_cooling_support( assert climate_thermostat.attributes["current_temperature"] == 22.6 assert climate_thermostat.attributes["temperature"] == 22.0 assert hass.states.get("sensor.thermostat") is None - assert hass.states.get("sensor.thermostat_battery_level").state == "100" + assert hass.states.get("sensor.thermostat_battery").state == "100" assert hass.states.get("climate.presence_sensor") is None assert hass.states.get("climate.clip_thermostat") is None @@ -385,7 +385,7 @@ async def test_climate_device_with_cooling_support( ] assert climate_thermostat.attributes["current_temperature"] == 23.2 assert climate_thermostat.attributes["temperature"] == 22.2 - assert hass.states.get("sensor.zen_01_battery_level").state == "25" + assert hass.states.get("sensor.zen_01_battery").state == "25" # Event signals thermostat state cool @@ -787,4 +787,4 @@ async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocke assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO - assert hass.states.get("sensor.thermostat_battery_level").state == "100" + assert hass.states.get("sensor.thermostat_battery").state == "100" diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 76babab36beba..1d3c4f7a811f5 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -80,9 +80,9 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket): assert ( len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 7 ) - assert hass.states.get("sensor.switch_2_battery_level").state == "100" - assert hass.states.get("sensor.switch_3_battery_level").state == "100" - assert hass.states.get("sensor.switch_4_battery_level").state == "100" + assert hass.states.get("sensor.switch_2_battery").state == "100" + assert hass.states.get("sensor.switch_3_battery").state == "100" + assert hass.states.get("sensor.switch_4_battery").state == "100" captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 15e63a6a81f22..4ae8fd32e45bd 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -120,7 +120,7 @@ async def test_get_triggers(hass, aioclient_mock): { CONF_DEVICE_ID: device.id, CONF_DOMAIN: SENSOR_DOMAIN, - ATTR_ENTITY_ID: "sensor.tradfri_on_off_switch_battery_level", + ATTR_ENTITY_ID: "sensor.tradfri_on_off_switch_battery", CONF_PLATFORM: "device", CONF_TYPE: ATTR_BATTERY_LEVEL, }, diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 17da9f1141a22..d0905f5ba5fc4 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -49,6 +49,7 @@ async def test_entry_diagnostics( "entities": { str(Platform.ALARM_CONTROL_PANEL): [], str(Platform.BINARY_SENSOR): [], + str(Platform.BUTTON): [], str(Platform.CLIMATE): [], str(Platform.COVER): [], str(Platform.FAN): [], diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 30473814f2647..3a4f6b907af21 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,7 +1,8 @@ """Test deCONZ gateway.""" +import asyncio from copy import deepcopy -from unittest.mock import Mock, patch +from unittest.mock import patch import pydeconz from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING @@ -12,13 +13,14 @@ DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect from homeassistant.components.deconz.gateway import ( - get_gateway, + get_deconz_session, get_gateway_from_config_entry, ) from homeassistant.components.fan import DOMAIN as FAN_DOMAIN @@ -159,16 +161,17 @@ async def test_gateway_setup(hass, aioclient_mock): config_entry, BINARY_SENSOR_DOMAIN, ) - assert forward_entry_setup.mock_calls[2][1] == (config_entry, CLIMATE_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (config_entry, COVER_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (config_entry, FAN_DOMAIN) - assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_DOMAIN) - assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN) - assert forward_entry_setup.mock_calls[7][1] == (config_entry, NUMBER_DOMAIN) - assert forward_entry_setup.mock_calls[8][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[9][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[10][1] == (config_entry, SIREN_DOMAIN) - assert forward_entry_setup.mock_calls[11][1] == (config_entry, SWITCH_DOMAIN) + assert forward_entry_setup.mock_calls[2][1] == (config_entry, BUTTON_DOMAIN) + assert forward_entry_setup.mock_calls[3][1] == (config_entry, CLIMATE_DOMAIN) + assert forward_entry_setup.mock_calls[4][1] == (config_entry, COVER_DOMAIN) + assert forward_entry_setup.mock_calls[5][1] == (config_entry, FAN_DOMAIN) + assert forward_entry_setup.mock_calls[6][1] == (config_entry, LIGHT_DOMAIN) + assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN) + assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN) + assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN) + assert forward_entry_setup.mock_calls[10][1] == (config_entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[11][1] == (config_entry, SIREN_DOMAIN) + assert forward_entry_setup.mock_calls[12][1] == (config_entry, SWITCH_DOMAIN) device_registry = dr.async_get(hass) gateway_entry = device_registry.async_get_device( @@ -200,25 +203,6 @@ async def test_gateway_device_configuration_url_when_addon(hass, aioclient_mock) ) -async def test_gateway_retry(hass): - """Retry setup.""" - with patch( - "homeassistant.components.deconz.gateway.get_gateway", - side_effect=CannotConnect, - ): - await setup_deconz_integration(hass) - assert not hass.data[DECONZ_DOMAIN] - - -async def test_gateway_setup_fails(hass): - """Retry setup.""" - with patch( - "homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception - ): - await setup_deconz_integration(hass) - assert not hass.data[DECONZ_DOMAIN] - - async def test_connection_status_signalling( hass, aioclient_mock, mock_deconz_websocket ): @@ -280,18 +264,6 @@ async def test_update_address(hass, aioclient_mock): assert len(mock_setup_entry.mock_calls) == 1 -async def test_gateway_trigger_reauth_flow(hass): - """Failed authentication trigger a reauthentication flow.""" - with patch( - "homeassistant.components.deconz.gateway.get_gateway", - side_effect=AuthenticationRequired, - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: - await setup_deconz_integration(hass) - mock_flow_init.assert_called_once() - - assert hass.data[DECONZ_DOMAIN] == {} - - async def test_reset_after_successful_setup(hass, aioclient_mock): """Make sure that connection status triggers a dispatcher send.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) @@ -303,25 +275,24 @@ async def test_reset_after_successful_setup(hass, aioclient_mock): assert result is True -async def test_get_gateway(hass): +async def test_get_deconz_session(hass): """Successful call.""" with patch("pydeconz.DeconzSession.refresh_state", return_value=True): - assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + assert await get_deconz_session(hass, ENTRY_CONFIG) -async def test_get_gateway_fails_unauthorized(hass): - """Failed call.""" - with patch( - "pydeconz.DeconzSession.refresh_state", - side_effect=pydeconz.errors.Unauthorized, - ), pytest.raises(AuthenticationRequired): - assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False - - -async def test_get_gateway_fails_cannot_connect(hass): +@pytest.mark.parametrize( + "side_effect, raised_exception", + [ + (asyncio.TimeoutError, CannotConnect), + (pydeconz.RequestError, CannotConnect), + (pydeconz.Unauthorized, AuthenticationRequired), + ], +) +async def test_get_deconz_session_fails(hass, side_effect, raised_exception): """Failed call.""" with patch( "pydeconz.DeconzSession.refresh_state", - side_effect=pydeconz.errors.RequestError, - ), pytest.raises(CannotConnect): - assert await get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False + side_effect=side_effect, + ), pytest.raises(raised_exception): + assert await get_deconz_session(hass, ENTRY_CONFIG) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index e50ac41d63de8..05fb708b75f41 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,6 +1,5 @@ """Test deCONZ component setup process.""" -import asyncio from unittest.mock import patch from homeassistant.components.deconz import ( @@ -13,6 +12,7 @@ CONF_GROUP_ID_BASE, DOMAIN as DECONZ_DOMAIN, ) +from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import entity_registry as er @@ -42,29 +42,36 @@ async def setup_entry(hass, entry): assert await async_setup_entry(hass, entry) is True -async def test_setup_entry_fails(hass): - """Test setup entry fails if deCONZ is not available.""" - with patch("pydeconz.DeconzSession.refresh_state", side_effect=Exception): - await setup_deconz_integration(hass) - assert not hass.data[DECONZ_DOMAIN] +async def test_setup_entry_successful(hass, aioclient_mock): + """Test setup entry is successful.""" + config_entry = await setup_deconz_integration(hass, aioclient_mock) + assert hass.data[DECONZ_DOMAIN] + assert config_entry.entry_id in hass.data[DECONZ_DOMAIN] + assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master -async def test_setup_entry_no_available_bridge(hass): - """Test setup entry fails if deCONZ is not available.""" + +async def test_setup_entry_fails_config_entry_not_ready(hass): + """Failed authentication trigger a reauthentication flow.""" with patch( - "pydeconz.DeconzSession.refresh_state", side_effect=asyncio.TimeoutError + "homeassistant.components.deconz.get_deconz_session", + side_effect=CannotConnect, ): await setup_deconz_integration(hass) - assert not hass.data[DECONZ_DOMAIN] + assert hass.data[DECONZ_DOMAIN] == {} -async def test_setup_entry_successful(hass, aioclient_mock): - """Test setup entry is successful.""" - config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert hass.data[DECONZ_DOMAIN] - assert config_entry.entry_id in hass.data[DECONZ_DOMAIN] - assert hass.data[DECONZ_DOMAIN][config_entry.entry_id].master +async def test_setup_entry_fails_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.deconz.get_deconz_session", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_deconz_integration(hass) + mock_flow_init.assert_called_once() + + assert hass.data[DECONZ_DOMAIN] == {} async def test_setup_entry_multiple_gateways(hass, aioclient_mock): diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 0cf0650e3d12b..e0c469a1ba2f9 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -10,6 +10,8 @@ SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from .test_gateway import ( DECONZ_WEB_REQUEST, @@ -24,29 +26,79 @@ async def test_no_number_entities(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): - """Test successful creation of binary sensor entities.""" - data = { - "sensors": { - "0": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"dark": False, "presence": False}, - "config": { - "delay": 0, - "on": True, - "reachable": True, - "temperature": 10, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", +TEST_DATA = [ + ( # Presence sensor - delay configuration + { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "delay": 0, + "on": True, + "reachable": True, + "temperature": 10, }, - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "number.presence_sensor_delay", + "unique_id": "00:00:00:00:00:00:00:00-delay", + "state": "0", + "entity_category": EntityCategory.CONFIG, + "attributes": { + "min": 0, + "max": 65535, + "step": 1, + "mode": "auto", + "friendly_name": "Presence sensor Delay", + }, + "websocket_event": {"config": {"delay": 10}}, + "next_state": "10", + "supported_service_value": 111, + "supported_service_response": {"delay": 111}, + "unsupported_service_value": 0.1, + "unsupported_service_response": {"delay": 0}, + "out_of_range_service_value": 66666, + }, + ) +] + + +@pytest.mark.parametrize("sensor_data, expected", TEST_DATA) +async def test_number_entities( + hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected +): + """Test successful creation of number entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 3 - assert hass.states.get("number.presence_sensor_delay").state == "0" + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify state data + + entity = hass.states.get(expected["entity_id"]) + assert entity.state == expected["state"] + assert entity.attributes == expected["attributes"] + + # Verify entity registry data + + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] + + # Verify device registry data + + assert ( + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] + ) + + # Change state event_changed_sensor = { "t": "event", @@ -57,8 +109,7 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() - - assert hass.states.get("number.presence_sensor_delay").state == "10" + assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Verify service calls @@ -69,20 +120,26 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 111}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["supported_service_value"], + }, blocking=True, ) - assert aioclient_mock.mock_calls[1][2] == {"delay": 111} + assert aioclient_mock.mock_calls[1][2] == expected["supported_service_response"] # Service set float value await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 0.1}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["unsupported_service_value"], + }, blocking=True, ) - assert aioclient_mock.mock_calls[2][2] == {"delay": 0} + assert aioclient_mock.mock_calls[2][2] == expected["unsupported_service_response"] # Service set value beyond the supported range @@ -90,15 +147,20 @@ async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 66666}, + { + ATTR_ENTITY_ID: expected["entity_id"], + ATTR_VALUE: expected["out_of_range_service_value"], + }, blocking=True, ) + # Unload entry + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE - assert hass.states.get("number.presence_sensor_delay").state == STATE_UNAVAILABLE + # Remove entry await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index e6f74cd0529f4..f28e83d3f3982 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -2,9 +2,12 @@ from unittest.mock import patch +import pytest + from homeassistant.components.deconz.gateway import get_gateway_from_config_entry from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from .test_gateway import ( @@ -20,45 +23,86 @@ async def test_no_scenes(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_scenes(hass, aioclient_mock): - """Test that scenes works.""" - data = { - "groups": { - "1": { - "id": "Light group id", - "name": "Light group", - "type": "LightGroup", - "state": {"all_on": False, "any_on": True}, - "action": {}, - "scenes": [{"id": "1", "name": "Scene"}], - "lights": [], +TEST_DATA = [ + ( # Scene + { + "groups": { + "1": { + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene"}], + "lights": [], + } } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "scene.light_group_scene", + "unique_id": "01234E56789A/groups/1/scenes/1", + "entity_category": None, + "attributes": { + "friendly_name": "Light group Scene", + }, + "request": "/groups/1/scenes/1/recall", + }, + ), +] + + +@pytest.mark.parametrize("raw_data, expected", TEST_DATA) +async def test_scenes(hass, aioclient_mock, raw_data, expected): + """Test successful creation of scene entities.""" + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + + with patch.dict(DECONZ_WEB_REQUEST, raw_data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("scene.light_group_scene") + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify state data + + scene = hass.states.get(expected["entity_id"]) + assert scene.attributes == expected["attributes"] + + # Verify entity registry data - # Verify service calls + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] - mock_deconz_put_request( - aioclient_mock, config_entry.data, "/groups/1/scenes/1/recall" + # Verify device registry data + + assert ( + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] ) - # Service turn on scene + # Verify button press + + mock_deconz_put_request(aioclient_mock, config_entry.data, expected["request"]) await hass.services.async_call( SCENE_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "scene.light_group_scene"}, + {ATTR_ENTITY_ID: expected["entity_id"]}, blocking=True, ) assert aioclient_mock.mock_calls[1][2] == {} + # Unload entry + await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE + # Remove entry + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 @@ -80,10 +124,10 @@ async def test_only_new_scenes_are_created(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 gateway = get_gateway_from_config_entry(hass, config_entry) async_dispatcher_send(hass, gateway.signal_new_scene) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index ee66a159c1802..bd51bab44e4e2 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -3,12 +3,17 @@ from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR -from homeassistant.components.deconz.sensor import ATTR_DAYLIGHT -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt @@ -23,159 +28,639 @@ async def test_no_sensors(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): - """Test successful creation of sensor entities.""" - data = { - "sensors": { - "1": { - "name": "Light level sensor", - "type": "ZHALightLevel", - "state": {"daylight": 6955, "lightlevel": 30000, "dark": False}, - "config": {"on": True, "reachable": True, "temperature": 10}, - "uniqueid": "00:00:00:00:00:00:00:00-00", +TEST_DATA = [ + ( # Air quality sensor + { + "config": { + "on": True, + "reachable": True, }, - "2": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"presence": False}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:01-00", + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "airqualityppb": 809, + "lastupdated": "2020-11-20T22:48:00.209", }, - "3": { - "name": "Switch 1", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:02-00", + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.bosch_air_quality_sensor", + "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef", + "state": "poor", + "entity_category": None, + "device_class": None, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "friendly_name": "BOSCH Air quality sensor", }, - "4": { - "name": "Switch 2", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:03-00", + "websocket_event": {"state": {"airquality": "excellent"}}, + "next_state": "excellent", + }, + ), + ( # Air quality PPB sensor + { + "config": { + "on": True, + "reachable": True, }, - "5": { - "name": "Power sensor", - "type": "ZHAPower", - "state": {"current": 2, "power": 6, "voltage": 3}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:05-00", + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "airqualityppb": 809, + "lastupdated": "2020-11-20T22:48:00.209", }, - "6": { - "name": "Consumption sensor", - "type": "ZHAConsumption", - "state": {"consumption": 2, "power": 6}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:06-00", + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.bosch_air_quality_sensor_ppb", + "unique_id": "00:12:4b:00:14:4d:00:07-ppb", + "state": "809", + "entity_category": None, + "device_class": SensorDeviceClass.AQI, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "unit_of_measurement": "ppb", + "device_class": "aqi", + "friendly_name": "BOSCH Air quality sensor PPB", }, - "7": { - "id": "CLIP light sensor id", - "name": "CLIP light level sensor", - "type": "CLIPLightLevel", - "state": {"lightlevel": 30000}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:07-00", + "websocket_event": {"state": {"airqualityppb": 1000}}, + "next_state": "1000", + }, + ), + ( # Battery sensor + { + "config": { + "alert": "none", + "on": True, + "reachable": True, }, - } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) + "ep": 1, + "etag": "23a8659f1cb22df2f51bc2da0e241bb4", + "manufacturername": "IKEA of Sweden", + "modelid": "FYRTUR block-out roller blind", + "name": "FYRTUR block-out roller blind", + "state": { + "battery": 100, + "lastupdated": "none", + }, + "swversion": "2.2.007", + "type": "ZHABattery", + "uniqueid": "00:0d:6f:ff:fe:01:23:45-01-0001", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.fyrtur_block_out_roller_blind_battery", + "unique_id": "00:0d:6f:ff:fe:01:23:45-battery", + "state": "100", + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "%", + "device_class": "battery", + "friendly_name": "FYRTUR block-out roller blind Battery", + }, + "websocket_event": {"state": {"battery": 50}}, + "next_state": "50", + }, + ), + ( # Consumption sensor + { + "config": {"on": True, "reachable": True}, + "ep": 1, + "etag": "a99e5bc463d15c23af7e89946e784cca", + "manufacturername": "Heiman", + "modelid": "SmartPlug", + "name": "Consumption 15", + "state": { + "consumption": 11342, + "lastupdated": "2018-03-12T19:19:08", + "power": 123, + }, + "type": "ZHAConsumption", + "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0702", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.consumption_15", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702", + "state": "11.342", + "entity_category": None, + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.TOTAL_INCREASING, + "attributes": { + "state_class": "total_increasing", + "on": True, + "power": 123, + "unit_of_measurement": "kWh", + "device_class": "energy", + "friendly_name": "Consumption 15", + }, + "websocket_event": {"state": {"consumption": 10000}}, + "next_state": "10.0", + }, + ), + ( # Daylight sensor + { + "config": { + "configured": True, + "on": True, + "sunriseoffset": 30, + "sunsetoffset": -30, + }, + "etag": "55047cf652a7e594d0ee7e6fae01dd38", + "manufacturername": "Philips", + "modelid": "PHDL00", + "name": "Daylight", + "state": { + "daylight": True, + "lastupdated": "2018-03-24T17:26:12", + "status": 170, + }, + "swversion": "1.0", + "type": "Daylight", + }, + { + "enable_entity": True, + "entity_count": 1, + "device_count": 2, + "entity_id": "sensor.daylight", + "unique_id": "", + "state": "solar_noon", + "entity_category": None, + "device_class": None, + "state_class": None, + "attributes": { + "on": True, + "daylight": True, + "icon": "mdi:white-balance-sunny", + "friendly_name": "Daylight", + }, + "websocket_event": {"state": {"status": 210}}, + "next_state": "dusk", + }, + ), + ( # Generic status sensor + { + "config": { + "on": True, + "reachable": True, + }, + "etag": "aacc83bc7d6e4af7e44014e9f776b206", + "manufacturername": "Phoscon", + "modelid": "PHOSCON_FSM_STATE", + "name": "FSM_STATE Motion stair", + "state": { + "lastupdated": "2019-04-24T00:00:25", + "status": 0, + }, + "swversion": "1.0", + "type": "CLIPGenericStatus", + "uniqueid": "fsm-state-1520195376277", + }, + { + "entity_count": 1, + "device_count": 2, + "entity_id": "sensor.fsm_state_motion_stair", + "unique_id": "fsm-state-1520195376277", + "state": "0", + "entity_category": None, + "device_class": None, + "state_class": None, + "attributes": { + "on": True, + "friendly_name": "FSM_STATE Motion stair", + }, + "websocket_event": {"state": {"status": 1}}, + "next_state": "1", + }, + ), + ( # Humidity sensor + { + "config": { + "battery": 100, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "humidity": 3555, + "lastupdated": "2019-05-05T14:39:00", + }, + "swversion": "20161129", + "type": "ZHAHumidity", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0405", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0405", + "state": "35.5", + "entity_category": None, + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "%", + "device_class": "humidity", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"humidity": 1000}}, + "next_state": "10.0", + }, + ), + ( # Light level sensor + { + "config": { + "alert": "none", + "battery": 100, + "ledindication": False, + "on": True, + "pending": [], + "reachable": True, + "tholddark": 12000, + "tholdoffset": 7000, + "usertest": False, + }, + "ep": 2, + "etag": "5cfb81765e86aa53ace427cfd52c6d52", + "manufacturername": "Philips", + "modelid": "SML001", + "name": "Motion sensor 4", + "state": { + "dark": True, + "daylight": False, + "lastupdated": "2019-05-05T14:37:06", + "lightlevel": 6955, + "lux": 5, + }, + "swversion": "6.1.0.18912", + "type": "ZHALightLevel", + "uniqueid": "00:17:88:01:03:28:8c:9b-02-0400", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.motion_sensor_4", + "unique_id": "00:17:88:01:03:28:8c:9b-02-0400", + "state": "5.0", + "entity_category": None, + "device_class": SensorDeviceClass.ILLUMINANCE, + "state_class": None, + "attributes": { + "on": True, + "dark": True, + "daylight": False, + "unit_of_measurement": "lx", + "device_class": "illuminance", + "friendly_name": "Motion sensor 4", + }, + "websocket_event": {"state": {"lightlevel": 1000}}, + "next_state": "1.3", + }, + ), + ( # Power sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "96e71c7db4685b334d3d0decc3f11868", + "manufacturername": "Heiman", + "modelid": "SmartPlug", + "name": "Power 16", + "state": { + "current": 34, + "lastupdated": "2018-03-12T19:22:13", + "power": 64, + "voltage": 231, + }, + "type": "ZHAPower", + "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0b04", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.power_16", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04", + "state": "64", + "entity_category": None, + "device_class": SensorDeviceClass.POWER, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "current": 34, + "voltage": 231, + "unit_of_measurement": "W", + "device_class": "power", + "friendly_name": "Power 16", + }, + "websocket_event": {"state": {"power": 1000}}, + "next_state": "1000", + }, + ), + ( # Pressure sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "lastupdated": "2019-05-05T14:39:00", + "pressure": 1010, + }, + "swversion": "20161129", + "type": "ZHAPressure", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0403", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0403", + "state": "1010", + "entity_category": None, + "device_class": SensorDeviceClass.PRESSURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "hPa", + "device_class": "pressure", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"pressure": 500}}, + "next_state": "500", + }, + ), + ( # Temperature sensor + { + "config": { + "battery": 100, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "lastupdated": "2019-05-05T14:39:00", + "temperature": 2182, + }, + "swversion": "20161129", + "type": "ZHATemperature", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0402", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0402", + "state": "21.8", + "entity_category": None, + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "°C", + "device_class": "temperature", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"temperature": 1800}}, + "next_state": "18.0", + }, + ), + ( # Time sensor + { + "config": { + "battery": 40, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "28e796678d9a24712feef59294343bb6", + "lastseen": "2020-11-22T11:26Z", + "manufacturername": "Danfoss", + "modelid": "eTRV0100", + "name": "eTRV Séjour", + "state": { + "lastset": "2020-11-19T08:07:08Z", + "lastupdated": "2020-11-22T10:51:03.444", + "localtime": "2020-11-22T10:51:01", + "utc": "2020-11-22T10:51:01Z", + }, + "swversion": "20200429", + "type": "ZHATime", + "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.etrv_sejour", + "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + "state": "2020-11-19T08:07:08+00:00", + "entity_category": None, + "device_class": SensorDeviceClass.TIMESTAMP, + "state_class": SensorStateClass.TOTAL_INCREASING, + "attributes": { + "state_class": "total_increasing", + "device_class": "timestamp", + "friendly_name": "eTRV Séjour", + }, + "websocket_event": {"state": {"lastset": "2020-12-14T10:12:14Z"}}, + "next_state": "2020-12-14T10:12:14+00:00", + }, + ), + ( # Secondary temperature sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + "temperature": 2600, + }, + "ep": 1, + "etag": "18c0f3c2100904e31a7f938db2ba9ba9", + "manufacturername": "dresden elektronik", + "modelid": "lumi.sensor_motion.aq2", + "name": "Alarm 10", + "state": { + "alarm": False, + "lastupdated": "none", + "lowbattery": None, + "tampered": None, + }, + "swversion": "20170627", + "type": "ZHAAlarm", + "uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "sensor.alarm_10_temperature", + "unique_id": "00:15:8d:00:02:b5:d1:80-temperature", + "state": "26.0", + "entity_category": None, + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "unit_of_measurement": "°C", + "device_class": "temperature", + "friendly_name": "Alarm 10 Temperature", + }, + "websocket_event": {"state": {"temperature": 1800}}, + "next_state": "26.0", + }, + ), + ( # Battery from switch + { + "config": { + "battery": 90, + "group": "201", + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "233ae541bbb7ac98c42977753884b8d2", + "manufacturername": "Philips", + "mode": 1, + "modelid": "RWL021", + "name": "Dimmer switch 3", + "state": { + "buttonevent": 1002, + "lastupdated": "2019-04-28T20:29:13", + }, + "swversion": "5.45.1.17846", + "type": "ZHASwitch", + "uniqueid": "00:17:88:01:02:0e:32:a3-02-fc00", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.dimmer_switch_3_battery", + "unique_id": "00:17:88:01:02:0e:32:a3-battery", + "state": "90", + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "event_id": "dimmer_switch_3", + "unit_of_measurement": "%", + "device_class": "battery", + "friendly_name": "Dimmer switch 3 Battery", + }, + "websocket_event": {"config": {"battery": 80}}, + "next_state": "80", + }, + ), +] - assert len(hass.states.async_all()) == 6 +@pytest.mark.parametrize("sensor_data, expected", TEST_DATA) +async def test_sensors( + hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected +): + """Test successful creation of sensor entities.""" ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) - light_level_sensor = hass.states.get("sensor.light_level_sensor") - assert light_level_sensor.state == "999.8" - assert ( - light_level_sensor.attributes[ATTR_DEVICE_CLASS] - == SensorDeviceClass.ILLUMINANCE - ) - assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955 + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): + config_entry = await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} + ) - light_level_temp = hass.states.get("sensor.light_level_sensor_temperature") - assert light_level_temp.state == "0.1" - assert ( - light_level_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE - ) + # Enable in entity registry + if expected.get("enable_entity"): + ent_reg.async_update_entity(entity_id=expected["entity_id"], disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == expected["entity_count"] - assert not hass.states.get("sensor.presence_sensor") - assert not hass.states.get("sensor.switch_1") - assert not hass.states.get("sensor.switch_1_battery_level") - assert not hass.states.get("sensor.switch_2") + # Verify entity state + sensor = hass.states.get(expected["entity_id"]) + assert sensor.state == expected["state"] + assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"] + assert sensor.attributes == expected["attributes"] - switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") - assert switch_2_battery_level.state == "100" + # Verify entity registry assert ( - switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] - == SensorDeviceClass.BATTERY + ent_reg.async_get(expected["entity_id"]).entity_category + is expected["entity_category"] ) + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] + + # Verify device registry assert ( - ent_reg.async_get("sensor.switch_2_battery_level").entity_category - == EntityCategory.DIAGNOSTIC + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] ) - assert not hass.states.get("sensor.daylight_sensor") - - power_sensor = hass.states.get("sensor.power_sensor") - assert power_sensor.state == "6" - assert power_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - - consumption_sensor = hass.states.get("sensor.consumption_sensor") - assert consumption_sensor.state == "0.002" - assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - - assert not hass.states.get("sensor.clip_light_level_sensor") - - # Event signals new light level - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "state": {"lightlevel": 2000}, - } - await mock_deconz_websocket(data=event_changed_sensor) + # Change state - assert hass.states.get("sensor.light_level_sensor").state == "1.6" - - # Event signals new temperature value - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "config": {"temperature": 100}, - } - await mock_deconz_websocket(data=event_changed_sensor) - - assert hass.states.get("sensor.light_level_sensor_temperature").state == "1.0" - - # Event signals new battery level - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "4", - "config": {"battery": 75}, - } + event_changed_sensor = {"t": "event", "e": "changed", "r": "sensors", "id": "1"} + event_changed_sensor |= expected["websocket_event"] await mock_deconz_websocket(data=event_changed_sensor) - - assert hass.states.get("sensor.switch_2_battery_level").state == "75" + await hass.async_block_till_done() + assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Unload entry await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 6 - for state in states: - assert state.state == STATE_UNAVAILABLE + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry @@ -184,6 +669,28 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 +async def test_not_allow_clip_sensor(hass, aioclient_mock): + """Test that CLIP sensors are not allowed.""" + data = { + "sensors": { + "1": { + "name": "CLIP temperature sensor", + "type": "CLIPTemperature", + "state": {"temperature": 2600}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + } + } + + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} + ) + + assert len(hass.states.async_all()) == 0 + + async def test_allow_clip_sensors(hass, aioclient_mock): """Test that CLIP sensors can be allowed.""" data = { @@ -295,7 +802,7 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 - assert not hass.states.get("sensor.switch_1_battery_level") + assert not hass.states.get("sensor.switch_1_battery") event_changed_sensor = { "t": "event", @@ -309,10 +816,11 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 1 - assert hass.states.get("sensor.switch_1_battery_level").state == "50" + assert hass.states.get("sensor.switch_1_battery").state == "50" -async def test_special_danfoss_battery_creation(hass, aioclient_mock): +@pytest.mark.parametrize("model_id", ["0x8030", "0x8031", "0x8034", "0x8035"]) +async def test_special_danfoss_battery_creation(hass, aioclient_mock, model_id): """Test the special Danfoss battery creation works. Normally there should only be one battery sensor per device from deCONZ. @@ -334,7 +842,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "982d9acc38bee5b251e24a9be26558e4", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:07.994", @@ -359,7 +867,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "62f12749f9f51c950086aff37dd02b61", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:22.399", @@ -384,7 +892,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "f50061174bb7f18a3d95789bab8b646d", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:25.466", @@ -409,7 +917,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "eea97adf8ce1b971b8b6a3a31793f96b", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:41.939", @@ -434,7 +942,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": {"lastupdated": "none", "on": False, "temperature": 2325}, "swversion": "YYYYMMDD", @@ -450,120 +958,6 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 -async def test_air_quality_sensor(hass, aioclient_mock): - """Test successful creation of air quality sensor entities.""" - data = { - "sensors": { - "0": { - "config": {"on": True, "reachable": True}, - "ep": 2, - "etag": "c2d2e42396f7c78e11e46c66e2ec0200", - "lastseen": "2020-11-20T22:48Z", - "manufacturername": "BOSCH", - "modelid": "AIR", - "name": "Air quality", - "state": { - "airquality": "poor", - "airqualityppb": 809, - "lastupdated": "2020-11-20T22:48:00.209", - }, - "swversion": "20200402", - "type": "ZHAAirQuality", - "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 2 - assert hass.states.get("sensor.air_quality").state == "poor" - assert hass.states.get("sensor.air_quality_ppb").state == "809" - - -async def test_daylight_sensor(hass, aioclient_mock): - """Test daylight sensor is disabled by default and when created has expected attributes.""" - data = { - "sensors": { - "0": { - "config": { - "configured": True, - "on": True, - "sunriseoffset": 30, - "sunsetoffset": -30, - }, - "etag": "55047cf652a7e594d0ee7e6fae01dd38", - "manufacturername": "Philips", - "modelid": "PHDL00", - "name": "Daylight sensor", - "state": { - "daylight": True, - "lastupdated": "2018-03-24T17:26:12", - "status": 170, - }, - "swversion": "1.0", - "type": "Daylight", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 0 - assert not hass.states.get("sensor.daylight_sensor") - - # Enable in entity registry - - entity_registry = er.async_get(hass) - entity_registry.async_update_entity( - entity_id="sensor.daylight_sensor", disabled_by=None - ) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - assert hass.states.get("sensor.daylight_sensor") - assert hass.states.get("sensor.daylight_sensor").attributes[ATTR_DAYLIGHT] - - -async def test_time_sensor(hass, aioclient_mock): - """Test successful creation of time sensor entities.""" - data = { - "sensors": { - "0": { - "config": {"battery": 40, "on": True, "reachable": True}, - "ep": 1, - "etag": "28e796678d9a24712feef59294343bb6", - "lastseen": "2020-11-22T11:26Z", - "manufacturername": "Danfoss", - "modelid": "eTRV0100", - "name": "Time", - "state": { - "lastset": "2020-11-19T08:07:08Z", - "lastupdated": "2020-11-22T10:51:03.444", - "localtime": "2020-11-22T10:51:01", - "utc": "2020-11-22T10:51:01Z", - }, - "swversion": "20200429", - "type": "ZHATime", - "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 2 - assert hass.states.get("sensor.time").state == "2020-11-19T08:07:08Z" - assert hass.states.get("sensor.time_battery_level").state == "40" - - async def test_unsupported_sensor(hass, aioclient_mock): """Test that unsupported sensors doesn't break anything.""" data = { diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 5cdd36440eaa6..086da5e24c433 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -254,7 +254,7 @@ async def test_service_refresh_devices(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 async def test_service_refresh_devices_trigger_no_state_update(hass, aioclient_mock): @@ -317,7 +317,7 @@ async def test_service_refresh_devices_trigger_no_state_update(hass, aioclient_m ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert len(captured_events) == 0 diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 03861a30c47b7..b93aef50774f6 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,5 +1,7 @@ """The tests for the derivative sensor platform.""" from datetime import timedelta +from math import sin +import random from unittest.mock import patch from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS @@ -180,6 +182,90 @@ async def test_data_moving_average_for_discrete_sensor(hass): assert abs(1 - derivative) <= 0.1 + 1e-6 +async def test_data_moving_average_for_irregular_times(hass): + """Test derivative sensor state.""" + # We simulate the following situation: + # The temperature rises 1 °C per minute for 30 minutes long. + # There is 60 random datapoints (and the start and end) and the signal is normally distributed + # around the expected value with ±0.1°C + # We use a time window of 1 minute and expect an error of less than the standard deviation. (0.01) + + time_window = 60 + random.seed(0) + times = sorted(random.sample(range(1800), 60)) + + def temp_function(time): + random.seed(0) + temp = time / (600) + return random.gauss(temp, 0.1) + + temperature_values = list(map(temp_function, times)) + + config, entity_id = await _setup_sensor( + hass, + { + "time_window": {"seconds": time_window}, + "unit_time": TIME_MINUTES, + "round": 3, + }, + ) + + base = dt_util.utcnow() + for time, value in zip(times, temperature_values): + now = base + timedelta(seconds=time) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + if time_window < time and time > times[3]: + state = hass.states.get("sensor.power") + derivative = round(float(state.state), config["sensor"]["round"]) + # Test that the error is never more than + # (time_window_in_minutes / true_derivative * 100) = 10% + ε + assert abs(0.1 - derivative) <= 0.01 + 1e-6 + + +async def test_double_signal_after_delay(hass): + """Test derivative sensor state.""" + # The old algorithm would produce extreme values if, after a delay longer than the time window + # there would be two signals, a large spike would be produced. Check explicitly for this situation + time_window = 60 + times = [*range(time_window * 10)] + times = times + [ + time_window * 20, + time_window * 20 + 0.01, + ] + + # just apply sine as some sort of temperature change and make sure the change after the delay is very small + temperature_values = [sin(x) for x in times] + temperature_values[-2] = temperature_values[-3] + 0.01 + temperature_values[-1] = temperature_values[-2] + 0.01 + + config, entity_id = await _setup_sensor( + hass, + { + "time_window": {"seconds": time_window}, + "unit_time": TIME_MINUTES, + "round": 3, + }, + ) + + base = dt_util.utcnow() + previous = 0 + for time, value in zip(times, temperature_values): + now = base + timedelta(seconds=time) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + state = hass.states.get("sensor.power") + derivative = round(float(state.state), config["sensor"]["round"]) + if time == times[-1]: + # Test that the error is never more than + # (time_window_in_minutes / true_derivative * 100) = 10% + ε + assert abs(previous - derivative) <= 0.01 + 1e-6 + previous = derivative + + async def test_prefix(hass): """Test derivative sensor state using a power source.""" config = { diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 3c8efad5b05f3..5134123074ef0 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -1,8 +1,15 @@ """Test Device Tracker config entry things.""" from homeassistant.components.device_tracker import DOMAIN, config_entry as ce +from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from tests.common import MockConfigEntry +from tests.common import ( + MockConfigEntry, + MockEntityPlatform, + MockPlatform, + mock_registry, +) def test_tracker_entity(): @@ -128,3 +135,87 @@ async def test_register_mac(hass): entity_entry_1 = ent_reg.async_get(entity_entry_1.entity_id) assert entity_entry_1.disabled_by is None + + +async def test_connected_device_registered(hass): + """Test dispatch on connected device being registered.""" + + registry = mock_registry(hass) + dispatches = [] + + @callback + def _save_dispatch(msg): + dispatches.append(msg) + + unsub = async_dispatcher_connect( + hass, ce.CONNECTED_DEVICE_REGISTERED, _save_dispatch + ) + + class MockScannerEntity(ce.ScannerEntity): + """Mock a scanner entity.""" + + @property + def ip_address(self) -> str: + return "5.4.3.2" + + @property + def unique_id(self) -> str: + return self.mac_address + + class MockDisconnectedScannerEntity(MockScannerEntity): + """Mock a disconnected scanner entity.""" + + @property + def mac_address(self) -> str: + return "aa:bb:cc:dd:ee:ff" + + @property + def is_connected(self) -> bool: + return True + + @property + def hostname(self) -> str: + return "connected" + + class MockConnectedScannerEntity(MockScannerEntity): + """Mock a disconnected scanner entity.""" + + @property + def mac_address(self) -> str: + return "aa:bb:cc:dd:ee:00" + + @property + def is_connected(self) -> bool: + return False + + @property + def hostname(self) -> str: + return "disconnected" + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [MockConnectedScannerEntity(), MockDisconnectedScannerEntity()] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + full_name = f"{entity_platform.domain}.{config_entry.domain}" + assert full_name in hass.config.components + assert len(hass.states.async_entity_ids()) == 0 # should be disabled + assert len(registry.entities) == 2 + assert ( + registry.entities["test_domain.test_aa_bb_cc_dd_ee_ff"].config_entry_id + == "super-mock-id" + ) + unsub() + assert dispatches == [ + {"ip": "5.4.3.2", "mac": "aa:bb:cc:dd:ee:ff", "host_name": "connected"} + ] diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index bf72cb3411976..0953fc67b0a12 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -298,7 +298,7 @@ async def test_entity_attributes( assert picture == attrs.get(ATTR_ENTITY_PICTURE) -@patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") +@patch("homeassistant.components.device_tracker.legacy.DeviceTracker.async_see") async def test_see_service(mock_see, hass, enable_custom_integrations): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): diff --git a/tests/components/devolo_home_control/const.py b/tests/components/devolo_home_control/const.py index 96686e204eb4e..96090195d20b7 100644 --- a/tests/components/devolo_home_control/const.py +++ b/tests/components/devolo_home_control/const.py @@ -4,6 +4,7 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="192.168.0.1", + addresses=["192.168.0.1"], port=14791, hostname="test.local.", type="_dvl-deviceapi._tcp.local.", @@ -21,6 +22,7 @@ DISCOVERY_INFO_WRONG_DEVOLO_DEVICE = zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, @@ -30,6 +32,7 @@ DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 681c2673dff35..0e48833a78b1a 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -18,6 +18,7 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=IP, + addresses=[IP], port=14791, hostname="test.local.", type="_dvl-deviceapi._tcp.local.", @@ -38,6 +39,7 @@ DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 0956230d7871c..a809d6eb5aba2 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -16,6 +16,7 @@ ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, + CONNECTED_DEVICE_REGISTERED, SOURCE_TYPE_ROUTER, ) from homeassistant.components.dhcp.const import DOMAIN @@ -25,10 +26,12 @@ STATE_HOME, STATE_NOT_HOME, ) +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed # connect b8:b7:f1:6d:b5:33 192.168.210.56 RAW_DHCP_REQUEST = ( @@ -207,6 +210,52 @@ async def test_dhcp_renewal_match_hostname_and_macaddress(hass): ) +async def test_registered_devices(hass): + """Test discovery flows are created for registered devices.""" + integration_matchers = [ + {"domain": "not-matching", "registered_devices": True}, + {"domain": "mock-domain", "registered_devices": True}, + ] + + packet = Ether(RAW_DHCP_RENEWAL) + + registry = dr.async_get(hass) + config_entry = MockConfigEntry(domain="mock-domain", data={}) + config_entry.add_to_hass(hass) + registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "50147903852c")}, + name="name", + ) + # Not enabled should not get flows + config_entry2 = MockConfigEntry(domain="mock-domain-2", data={}) + config_entry2.add_to_hass(hass) + registry.async_get_or_create( + config_entry_id=config_entry2.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "50147903852c")}, + name="name", + ) + + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + await async_handle_dhcp_packet(packet) + # Ensure no change is ignored + await async_handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.1.120", + hostname="irobot-ae9ec12dd3b04885bcbfa36afb01e1cc", + macaddress="50147903852c", + ) + + async def test_dhcp_match_hostname(hass): """Test matching based on hostname only.""" integration_matchers = [{"domain": "mock-domain", "hostname": "connect"}] @@ -583,6 +632,59 @@ async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): ) +async def test_device_tracker_registered(hass): + """Test matching based on hostname and macaddress when registered.""" + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + async_dispatcher_send( + hass, + CONNECTED_DEVICE_REGISTERED, + {"ip": "192.168.210.56", "mac": "b8b7f16db533", "host_name": "connect"}, + ) + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == { + "source": config_entries.SOURCE_DHCP + } + assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo( + ip="192.168.210.56", + hostname="connect", + macaddress="b8b7f16db533", + ) + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + +async def test_device_tracker_registered_hostname_none(hass): + """Test handle None hostname.""" + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + async_dispatcher_send( + hass, + CONNECTED_DEVICE_REGISTERED, + {"ip": "192.168.210.56", "mac": "b8b7f16db533", "host_name": None}, + ) + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + async def test_device_tracker_hostname_and_macaddress_after_start(hass): """Test matching based on hostname and macaddress after start.""" diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 3cb4b2a726a44..a9ac5946f307f 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import AsyncIterable, Mapping +from dataclasses import dataclass from datetime import timedelta from types import MappingProxyType from typing import Any @@ -15,6 +16,7 @@ UpnpResponseError, ) from async_upnp_client.profiles.dlna import PlayMode, TransportState +from didl_lite import didl_lite import pytest from homeassistant import const as ha_const @@ -29,6 +31,8 @@ from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.media_player import ATTR_TO_PROPERTY, const as mp_const from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN +from homeassistant.components.media_source.const import DOMAIN as MS_DOMAIN +from homeassistant.components.media_source.models import PlayMedia from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_dr @@ -418,7 +422,7 @@ async def test_feature_flags( ("can_stop", mp_const.SUPPORT_STOP), ("can_previous", mp_const.SUPPORT_PREVIOUS_TRACK), ("can_next", mp_const.SUPPORT_NEXT_TRACK), - ("has_play_media", mp_const.SUPPORT_PLAY_MEDIA), + ("has_play_media", mp_const.SUPPORT_PLAY_MEDIA | mp_const.SUPPORT_BROWSE_MEDIA), ("can_seek_rel_time", mp_const.SUPPORT_SEEK), ("has_presets", mp_const.SUPPORT_SELECT_SOUND_MODE), ] @@ -760,6 +764,90 @@ async def test_play_media_metadata( ) +async def test_play_media_local_source( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media with a media_id from a local media_source.""" + # Based on roku's test_services_play_media_local_source and cast's + # test_entity_browse_media + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", + mp_const.ATTR_MEDIA_CONTENT_ID: "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + }, + blocking=True, + ) + + assert dmr_device_mock.construct_play_media_metadata.await_count == 1 + assert ( + "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" + in dmr_device_mock.construct_play_media_metadata.call_args.kwargs["media_url"] + ) + assert dmr_device_mock.async_set_transport_uri.await_count == 1 + assert dmr_device_mock.async_play.await_count == 1 + call_args = dmr_device_mock.async_set_transport_uri.call_args.args + assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] + + +async def test_play_media_didl_metadata( + hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test play_media passes available DIDL-Lite metadata to the DMR.""" + + @dataclass + class DidlPlayMedia(PlayMedia): + """Playable media with DIDL metadata.""" + + didl_metadata: didl_lite.DidlObject + + didl_metadata = didl_lite.VideoItem( + id="120$22$33", + restricted="false", + title="Epic Sax Guy 10 Hours", + res=[ + didl_lite.Resource(uri="unused-URI", protocol_info="http-get:*:video/mp4:") + ], + ) + + play_media = DidlPlayMedia( + url="/media/local/Epic Sax Guy 10 Hours.mp4", + mime_type="video/mp4", + didl_metadata=didl_metadata, + ) + + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=play_media, + ): + await hass.services.async_call( + MP_DOMAIN, + mp_const.SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: mock_entity_id, + mp_const.ATTR_MEDIA_CONTENT_TYPE: "video/mp4", + mp_const.ATTR_MEDIA_CONTENT_ID: "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + }, + blocking=True, + ) + + assert dmr_device_mock.construct_play_media_metadata.await_count == 0 + assert dmr_device_mock.async_set_transport_uri.await_count == 1 + assert dmr_device_mock.async_play.await_count == 1 + call_args = dmr_device_mock.async_set_transport_uri.call_args.args + assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] + assert call_args[1] == "Epic Sax Guy 10 Hours" + assert call_args[2] == didl_lite.to_xml_string(didl_metadata).decode() + + async def test_shuffle_repeat_modes( hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str ) -> None: @@ -844,6 +932,88 @@ async def test_shuffle_repeat_modes( dmr_device_mock.async_set_play_mode.assert_not_awaited() +async def test_browse_media( + hass: HomeAssistant, hass_ws_client, dmr_device_mock: Mock, mock_entity_id: str +) -> None: + """Test the async_browse_media method.""" + # Based on cast's test_entity_browse_media + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + # DMR can play all media types + dmr_device_mock.sink_protocol_info = ["*"] + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + expected_child_video = { + "title": "Epic Sax Guy 10 Hours.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + assert expected_child_video in response["result"]["children"] + + expected_child_audio = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + assert expected_child_audio in response["result"]["children"] + + # Device can only play MIME type audio/mpeg and audio/vorbis + dmr_device_mock.sink_protocol_info = [ + "http-get:*:audio/mpeg:*", + "http-get:*:audio/vorbis:*", + ] + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + # Video file should not be shown + assert expected_child_video not in response["result"]["children"] + # Audio file should appear + assert expected_child_audio in response["result"]["children"] + + # Device does not specify what it can play + dmr_device_mock.sink_protocol_info = [] + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + # All files should be returned + assert expected_child_video in response["result"]["children"] + assert expected_child_audio in response["result"]["children"] + + async def test_playback_update_state( hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str ) -> None: diff --git a/tests/components/dlna_dms/__init__.py b/tests/components/dlna_dms/__init__.py new file mode 100644 index 0000000000000..ed53881052161 --- /dev/null +++ b/tests/components/dlna_dms/__init__.py @@ -0,0 +1 @@ +"""Tests for the DLNA MediaServer integration.""" diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py new file mode 100644 index 0000000000000..6764001be31d6 --- /dev/null +++ b/tests/components/dlna_dms/conftest.py @@ -0,0 +1,131 @@ +"""Fixtures for DLNA DMS tests.""" +from __future__ import annotations + +from collections.abc import AsyncGenerator, Iterable +from typing import Final +from unittest.mock import Mock, create_autospec, patch, seal + +from async_upnp_client import UpnpDevice, UpnpService +from async_upnp_client.utils import absolute_url +import pytest + +from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.dms import DlnaDmsData, get_domain_data +from homeassistant.const import CONF_DEVICE_ID, CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_DEVICE_HOST: Final = "192.88.99.21" +MOCK_DEVICE_BASE_URL: Final = f"http://{MOCK_DEVICE_HOST}" +MOCK_DEVICE_LOCATION: Final = MOCK_DEVICE_BASE_URL + "/dms_description.xml" +MOCK_DEVICE_NAME: Final = "Test Server Device" +MOCK_DEVICE_TYPE: Final = "urn:schemas-upnp-org:device:MediaServer:1" +MOCK_DEVICE_UDN: Final = "uuid:7bf34520-f034-4fa2-8d2d-2f709d4221ef" +MOCK_DEVICE_USN: Final = f"{MOCK_DEVICE_UDN}::{MOCK_DEVICE_TYPE}" +MOCK_SOURCE_ID: Final = "test_server_device" + +LOCAL_IP: Final = "192.88.99.1" +EVENT_CALLBACK_URL: Final = "http://192.88.99.1/notify" + +NEW_DEVICE_LOCATION: Final = "http://192.88.99.7" + "/dmr_description.xml" + + +@pytest.fixture +def upnp_factory_mock() -> Iterable[Mock]: + """Mock the UpnpFactory class to construct DMS-style UPnP devices.""" + with patch( + "homeassistant.components.dlna_dms.dms.UpnpFactory", + autospec=True, + spec_set=True, + ) as upnp_factory: + upnp_device = create_autospec(UpnpDevice, instance=True) + upnp_device.name = MOCK_DEVICE_NAME + upnp_device.udn = MOCK_DEVICE_UDN + upnp_device.device_url = MOCK_DEVICE_LOCATION + upnp_device.device_type = MOCK_DEVICE_TYPE + upnp_device.available = True + upnp_device.parent_device = None + upnp_device.root_device = upnp_device + upnp_device.all_devices = [upnp_device] + upnp_device.services = { + "urn:schemas-upnp-org:service:ContentDirectory:1": create_autospec( + UpnpService, + instance=True, + service_type="urn:schemas-upnp-org:service:ContentDirectory:1", + service_id="urn:upnp-org:serviceId:ContentDirectory", + ), + "urn:schemas-upnp-org:service:ConnectionManager:1": create_autospec( + UpnpService, + instance=True, + service_type="urn:schemas-upnp-org:service:ConnectionManager:1", + service_id="urn:upnp-org:serviceId:ConnectionManager", + ), + } + seal(upnp_device) + upnp_factory_instance = upnp_factory.return_value + upnp_factory_instance.async_create_device.return_value = upnp_device + + yield upnp_factory_instance + + +@pytest.fixture +async def domain_data_mock( + hass: HomeAssistant, aioclient_mock, upnp_factory_mock +) -> AsyncGenerator[DlnaDmsData, None]: + """Mock some global data used by this component. + + This includes network clients and library object factories. Mocking it + prevents network use. + + Yields the actual domain data, for ease of access + """ + with patch( + "homeassistant.components.dlna_dms.dms.AiohttpSessionRequester", autospec=True + ): + yield get_domain_data(hass) + + +@pytest.fixture +def config_entry_mock() -> MockConfigEntry: + """Mock a config entry for this platform.""" + mock_entry = MockConfigEntry( + unique_id=MOCK_DEVICE_USN, + domain=DOMAIN, + data={ + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_USN, + }, + title=MOCK_DEVICE_NAME, + ) + return mock_entry + + +@pytest.fixture +def dms_device_mock(upnp_factory_mock: Mock) -> Iterable[Mock]: + """Mock the async_upnp_client DMS device, initially connected.""" + with patch( + "homeassistant.components.dlna_dms.dms.DmsDevice", autospec=True + ) as constructor: + device = constructor.return_value + device.on_event = None + device.profile_device = upnp_factory_mock.async_create_device.return_value + device.icon = MOCK_DEVICE_BASE_URL + "/icon.jpg" + device.udn = "device_udn" + device.manufacturer = "device_manufacturer" + device.model_name = "device_model_name" + device.name = "device_name" + device.get_absolute_url.side_effect = lambda url: absolute_url( + MOCK_DEVICE_BASE_URL, url + ) + + yield device + + +@pytest.fixture(autouse=True) +def ssdp_scanner_mock() -> Iterable[Mock]: + """Mock the SSDP module.""" + with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner: + reg_callback = mock_scanner.return_value.async_register_callback + reg_callback.return_value = Mock(return_value=None) + yield mock_scanner.return_value diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py new file mode 100644 index 0000000000000..df8d55dbc250d --- /dev/null +++ b/tests/components/dlna_dms/test_config_flow.py @@ -0,0 +1,346 @@ +"""Test the DLNA DMS config flow.""" +from __future__ import annotations + +import dataclasses +from typing import Final +from unittest.mock import Mock + +from async_upnp_client import UpnpError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp +from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL +from homeassistant.core import HomeAssistant + +from .conftest import ( + MOCK_DEVICE_HOST, + MOCK_DEVICE_LOCATION, + MOCK_DEVICE_NAME, + MOCK_DEVICE_TYPE, + MOCK_DEVICE_UDN, + MOCK_DEVICE_USN, + NEW_DEVICE_LOCATION, +) + +from tests.common import MockConfigEntry + +# Auto-use the domain_data_mock and dms_device_mock fixtures for every test in this module +pytestmark = [ + pytest.mark.usefixtures("domain_data_mock"), + pytest.mark.usefixtures("dms_device_mock"), +] + +WRONG_DEVICE_TYPE: Final = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + +MOCK_ROOT_DEVICE_UDN: Final = "ROOT_DEVICE" + +MOCK_DISCOVERY: Final = ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={ + ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + ssdp.ATTR_UPNP_SERVICE_LIST: { + "service": [ + { + "SCPDURL": "/ContentDirectory/scpd.xml", + "controlURL": "/ContentDirectory/control.xml", + "eventSubURL": "/ContentDirectory/event.xml", + "serviceId": "urn:upnp-org:serviceId:ContentDirectory", + "serviceType": "urn:schemas-upnp-org:service:ContentDirectory:1", + }, + { + "SCPDURL": "/ConnectionManager/scpd.xml", + "controlURL": "/ConnectionManager/control.xml", + "eventSubURL": "/ConnectionManager/event.xml", + "serviceId": "urn:upnp-org:serviceId:ConnectionManager", + "serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1", + }, + ] + }, + }, + x_homeassistant_matching_domains={DOMAIN}, +) + + +async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: + """Test user-init'd flow, user selects discovered device.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_HOST} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_USN, + } + assert result["options"] == {} + + await hass.async_block_till_done() + + +async def test_user_flow_no_devices( + hass: HomeAssistant, ssdp_scanner_mock: Mock +) -> None: + """Test user-init'd flow, there's really no devices to choose from.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [], + [], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" + + +async def test_ssdp_flow_success(hass: HomeAssistant) -> None: + """Test that SSDP discovery with an available device works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_USN, + } + assert result["options"] == {} + + +async def test_ssdp_flow_unavailable( + hass: HomeAssistant, domain_data_mock: Mock +) -> None: + """Test that SSDP discovery with an unavailable device still succeeds. + + All the required information for configuration is obtained from the SSDP + message, there's no need to connect to the device to configure it. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_USN, + } + assert result["options"] == {} + + +async def test_ssdp_flow_existing( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test that SSDP discovery of existing config entry updates the URL.""" + config_entry_mock.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_st="mock_st", + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + upnp={ + ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + }, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION + + +async def test_ssdp_flow_duplicate_location( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test that discovery of device with URL matching existing entry gets aborted.""" + config_entry_mock.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION + + +async def test_ssdp_flow_bad_data( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test bad SSDP discovery information is rejected cleanly.""" + # Missing location + discovery = dataclasses.replace(MOCK_DISCOVERY, ssdp_location="") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "bad_ssdp" + + # Missing USN + discovery = dataclasses.replace(MOCK_DISCOVERY, ssdp_usn="") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "bad_ssdp" + + +async def test_duplicate_name( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test device with name same as another results in no error.""" + config_entry_mock.add_to_hass(hass) + + mock_entry_1 = MockConfigEntry( + unique_id="mock_entry_1", + domain=DOMAIN, + data={ + CONF_URL: "not-important", + CONF_DEVICE_ID: "not-important", + }, + title=MOCK_DEVICE_NAME, + ) + mock_entry_1.add_to_hass(hass) + + # New UDN, USN, and location to be sure it's a new device + new_device_udn = "uuid:7bf34520-f034-4fa2-8d2d-2f709d422000" + new_device_usn = f"{new_device_udn}::{MOCK_DEVICE_TYPE}" + new_device_location = "http://192.88.99.22/dms_description.xml" + discovery = dataclasses.replace( + MOCK_DISCOVERY, + ssdp_usn=new_device_usn, + ssdp_location=new_device_location, + ssdp_udn=new_device_udn, + ) + discovery.upnp = dict(discovery.upnp) + discovery.upnp[ssdp.ATTR_UPNP_UDN] = new_device_udn + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: new_device_location, + CONF_DEVICE_ID: new_device_usn, + } + assert result["options"] == {} + + +async def test_ssdp_flow_upnp_udn( + hass: HomeAssistant, config_entry_mock: MockConfigEntry +) -> None: + """Test that SSDP discovery ignores the root device's UDN.""" + config_entry_mock.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={ + ssdp.ATTR_UPNP_UDN: "DIFFERENT_ROOT_DEVICE", + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + }, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION + + +async def test_ssdp_missing_services(hass: HomeAssistant) -> None: + """Test SSDP ignores devices that are missing required services.""" + # No services defined at all + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = dict(discovery.upnp) + del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_dms" + + # ContentDirectory service is missing + discovery = dataclasses.replace(MOCK_DISCOVERY) + discovery.upnp = dict(discovery.upnp) + discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = { + "service": [ + service + for service in discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST]["service"] + if service.get("serviceId") != "urn:upnp-org:serviceId:ContentDirectory" + ] + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_dms" diff --git a/tests/components/dlna_dms/test_device_availability.py b/tests/components/dlna_dms/test_device_availability.py new file mode 100644 index 0000000000000..a0cfb3ab2d2ff --- /dev/null +++ b/tests/components/dlna_dms/test_device_availability.py @@ -0,0 +1,705 @@ +"""Test how the DmsDeviceSource handles available and unavailable devices.""" +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterable +import logging +from unittest.mock import ANY, DEFAULT, Mock, patch + +from async_upnp_client.exceptions import UpnpConnectionError, UpnpError +from didl_lite import didl_lite +import pytest + +from homeassistant.components import ssdp +from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.dms import DmsDeviceSource, get_domain_data +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .conftest import ( + MOCK_DEVICE_LOCATION, + MOCK_DEVICE_NAME, + MOCK_DEVICE_TYPE, + MOCK_DEVICE_UDN, + MOCK_DEVICE_USN, + NEW_DEVICE_LOCATION, +) + +from tests.common import MockConfigEntry + +# Auto-use the domain_data_mock for every test in this module +pytestmark = [ + pytest.mark.usefixtures("domain_data_mock"), +] + + +async def setup_mock_component( + hass: HomeAssistant, mock_entry: MockConfigEntry +) -> DmsDeviceSource: + """Set up a mock DlnaDmrEntity with the given configuration.""" + mock_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + domain_data = get_domain_data(hass) + return next(iter(domain_data.devices.values())) + + +@pytest.fixture +async def connected_source_mock( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dms_device_mock: Mock, +) -> AsyncIterable[DmsDeviceSource]: + """Fixture to set up a mock DmsDeviceSource in a connected state. + + Yields the entity. Cleans up the entity after the test is complete. + """ + entity = await setup_mock_component(hass, config_entry_mock) + + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + + # Run the test + yield entity + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + + +@pytest.fixture +async def disconnected_source_mock( + hass: HomeAssistant, + upnp_factory_mock: Mock, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dms_device_mock: Mock, +) -> AsyncIterable[DmsDeviceSource]: + """Fixture to set up a mock DmsDeviceSource in a disconnected state. + + Yields the entity. Cleans up the entity after the test is complete. + """ + # Cause the connection attempt to fail + upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError + + entity = await setup_mock_component(hass, config_entry_mock) + + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + + # Run the test + yield entity + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + + +async def test_unavailable_device( + hass: HomeAssistant, + upnp_factory_mock: Mock, + ssdp_scanner_mock: Mock, + config_entry_mock: MockConfigEntry, +) -> None: + """Test a DlnaDmsEntity with out a connected DmsDevice.""" + # Cause connection attempts to fail + upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError + + with patch( + "homeassistant.components.dlna_dms.dms.DmsDevice", autospec=True + ) as dms_device_constructor_mock: + connected_source_mock = await setup_mock_component(hass, config_entry_mock) + + # Check device is not created + dms_device_constructor_mock.assert_not_called() + + # Check attempt was made to create a device from the supplied URL + upnp_factory_mock.async_create_device.assert_awaited_once_with(MOCK_DEVICE_LOCATION) + # Check SSDP notifications are registered + ssdp_scanner_mock.async_register_callback.assert_any_call( + ANY, {"USN": MOCK_DEVICE_USN} + ) + ssdp_scanner_mock.async_register_callback.assert_any_call( + ANY, {"_udn": MOCK_DEVICE_UDN, "NTS": "ssdp:byebye"} + ) + # Quick check of the state to verify the entity has no connected DmsDevice + assert not connected_source_mock.available + # Check the name matches that supplied + assert connected_source_mock.name == MOCK_DEVICE_NAME + + # Check attempts to browse and resolve media give errors + with pytest.raises(BrowseError): + await connected_source_mock.async_browse_media("/browse_path") + with pytest.raises(BrowseError): + await connected_source_mock.async_browse_media(":browse_object") + with pytest.raises(BrowseError): + await connected_source_mock.async_browse_media("?browse_search") + with pytest.raises(Unresolvable): + await connected_source_mock.async_resolve_media("/resolve_path") + with pytest.raises(Unresolvable): + await connected_source_mock.async_resolve_media(":resolve_object") + with pytest.raises(Unresolvable): + await connected_source_mock.async_resolve_media("?resolve_search") + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Confirm SSDP notifications unregistered + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + + +async def test_become_available( + hass: HomeAssistant, + upnp_factory_mock: Mock, + ssdp_scanner_mock: Mock, + config_entry_mock: MockConfigEntry, + dms_device_mock: Mock, +) -> None: + """Test a device becoming available after the entity is constructed.""" + # Cause connection attempts to fail before adding the entity + upnp_factory_mock.async_create_device.side_effect = UpnpConnectionError + connected_source_mock = await setup_mock_component(hass, config_entry_mock) + assert not connected_source_mock.available + + # Mock device is now available. + upnp_factory_mock.async_create_device.side_effect = None + upnp_factory_mock.async_create_device.reset_mock() + + # Send an SSDP notification from the now alive device + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Check device was created from the supplied URL + upnp_factory_mock.async_create_device.assert_awaited_once_with(NEW_DEVICE_LOCATION) + # Quick check of the state to verify the entity has a connected DmsDevice + assert connected_source_mock.available + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Confirm SSDP notifications unregistered + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + + +async def test_alive_but_gone( + hass: HomeAssistant, + upnp_factory_mock: Mock, + ssdp_scanner_mock: Mock, + disconnected_source_mock: DmsDeviceSource, +) -> None: + """Test a device sending an SSDP alive announcement, but not being connectable.""" + upnp_factory_mock.async_create_device.side_effect = UpnpError + + # Send an SSDP notification from the still missing device + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # There should be a connection attempt to the device + upnp_factory_mock.async_create_device.assert_awaited() + + # Device should still be unavailable + assert not disconnected_source_mock.available + + # Send the same SSDP notification, expecting no extra connection attempts + upnp_factory_mock.async_create_device.reset_mock() + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + upnp_factory_mock.async_create_device.assert_not_called() + upnp_factory_mock.async_create_device.assert_not_awaited() + assert not disconnected_source_mock.available + + # Send an SSDP notification with a new BOOTID, indicating the device has rebooted + upnp_factory_mock.async_create_device.reset_mock() + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Rebooted device (seen via BOOTID) should mean a new connection attempt + upnp_factory_mock.async_create_device.assert_awaited() + assert not disconnected_source_mock.available + + # Send byebye message to indicate device is going away. Next alive message + # should result in a reconnect attempt even with same BOOTID. + upnp_factory_mock.async_create_device.reset_mock() + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.BYEBYE, + ) + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Rebooted device (seen via byebye/alive) should mean a new connection attempt + upnp_factory_mock.async_create_device.assert_awaited() + assert not disconnected_source_mock.available + + +async def test_multiple_ssdp_alive( + hass: HomeAssistant, + upnp_factory_mock: Mock, + ssdp_scanner_mock: Mock, + disconnected_source_mock: DmsDeviceSource, +) -> None: + """Test multiple SSDP alive notifications is ok, only connects to device once.""" + upnp_factory_mock.async_create_device.reset_mock() + + # Contacting the device takes long enough that 2 simultaneous attempts could be made + async def create_device_delayed(_location): + """Delay before continuing with async_create_device. + + This gives a chance for parallel calls to `device_connect` to occur. + """ + await asyncio.sleep(0.1) + return DEFAULT + + upnp_factory_mock.async_create_device.side_effect = create_device_delayed + + # Send two SSDP notifications with the new device URL + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=NEW_DEVICE_LOCATION, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Check device is contacted exactly once + upnp_factory_mock.async_create_device.assert_awaited_once_with(NEW_DEVICE_LOCATION) + + # Device should be available + assert disconnected_source_mock.available + + +async def test_ssdp_byebye( + hass: HomeAssistant, + ssdp_scanner_mock: Mock, + connected_source_mock: DmsDeviceSource, +) -> None: + """Test device is disconnected when byebye is received.""" + # First byebye will cause a disconnect + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={"NTS": "ssdp:byebye"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.BYEBYE, + ) + + # Device should be gone + assert not connected_source_mock.available + + # Second byebye will do nothing + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={"NTS": "ssdp:byebye"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.BYEBYE, + ) + + +async def test_ssdp_update_seen_bootid( + hass: HomeAssistant, + ssdp_scanner_mock: Mock, + upnp_factory_mock: Mock, + disconnected_source_mock: DmsDeviceSource, +) -> None: + """Test device does not reconnect when it gets ssdp:update with next bootid.""" + # Start with a disconnected device + entity = disconnected_source_mock + assert not entity.available + + # "Reconnect" the device + upnp_factory_mock.async_create_device.reset_mock() + upnp_factory_mock.async_create_device.side_effect = None + + # Send SSDP alive with boot ID + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Device should be connected + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send SSDP update with next boot ID + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "1", + ssdp.ATTR_SSDP_NEXTBOOTID: "2", + }, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.UPDATE, + ) + await hass.async_block_till_done() + + # Device was not reconnected, even with a new boot ID + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send SSDP update with same next boot ID, again + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "1", + ssdp.ATTR_SSDP_NEXTBOOTID: "2", + }, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.UPDATE, + ) + await hass.async_block_till_done() + + # Nothing should change + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send SSDP update with bad next boot ID + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "2", + ssdp.ATTR_SSDP_NEXTBOOTID: "7c848375-a106-4bd1-ac3c-8e50427c8e4f", + }, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.UPDATE, + ) + await hass.async_block_till_done() + + # Nothing should change + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send a new SSDP alive with the new boot ID, device should not reconnect + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + +async def test_ssdp_update_missed_bootid( + hass: HomeAssistant, + ssdp_scanner_mock: Mock, + upnp_factory_mock: Mock, + disconnected_source_mock: DmsDeviceSource, +) -> None: + """Test device disconnects when it gets ssdp:update bootid it wasn't expecting.""" + # Start with a disconnected device + entity = disconnected_source_mock + assert not entity.available + + # "Reconnect" the device + upnp_factory_mock.async_create_device.reset_mock() + upnp_factory_mock.async_create_device.side_effect = None + + # Send SSDP alive with boot ID + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + # Device should be connected + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send SSDP update with skipped boot ID (not previously seen) + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_udn=MOCK_DEVICE_UDN, + ssdp_headers={ + "NTS": "ssdp:update", + ssdp.ATTR_SSDP_BOOTID: "2", + ssdp.ATTR_SSDP_NEXTBOOTID: "3", + }, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.UPDATE, + ) + await hass.async_block_till_done() + + # Device should not *re*-connect yet + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send a new SSDP alive with the new boot ID, device should reconnect + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "3"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 2 + + +async def test_ssdp_bootid( + hass: HomeAssistant, + upnp_factory_mock: Mock, + ssdp_scanner_mock: Mock, + disconnected_source_mock: DmsDeviceSource, +) -> None: + """Test an alive with a new BOOTID.UPNP.ORG header causes a reconnect.""" + # Start with a disconnected device + entity = disconnected_source_mock + assert not entity.available + + # "Reconnect" the device + upnp_factory_mock.async_create_device.side_effect = None + upnp_factory_mock.async_create_device.reset_mock() + + # Send SSDP alive with boot ID + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send SSDP alive with same boot ID, nothing should happen + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "1"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 1 + + # Send a new SSDP alive with an incremented boot ID, device should be dis/reconnected + await ssdp_callback( + ssdp.SsdpServiceInfo( + ssdp_usn=MOCK_DEVICE_USN, + ssdp_location=MOCK_DEVICE_LOCATION, + ssdp_headers={ssdp.ATTR_SSDP_BOOTID: "2"}, + ssdp_st=MOCK_DEVICE_TYPE, + upnp={}, + ), + ssdp.SsdpChange.ALIVE, + ) + await hass.async_block_till_done() + + assert entity.available + assert upnp_factory_mock.async_create_device.await_count == 2 + + +async def test_repeated_connect( + caplog: pytest.LogCaptureFixture, + connected_source_mock: DmsDeviceSource, + upnp_factory_mock: Mock, +) -> None: + """Test trying to connect an already connected device is safely ignored.""" + upnp_factory_mock.async_create_device.reset_mock() + # Calling internal function directly to skip trying to time 2 SSDP messages carefully + with caplog.at_level(logging.DEBUG): + await connected_source_mock.device_connect() + assert ( + "Trying to connect when device already connected" == caplog.records[-1].message + ) + assert not upnp_factory_mock.async_create_device.await_count + + +async def test_connect_no_location( + caplog: pytest.LogCaptureFixture, + disconnected_source_mock: DmsDeviceSource, + upnp_factory_mock: Mock, +) -> None: + """Test trying to connect without a location is safely ignored.""" + disconnected_source_mock.location = "" + upnp_factory_mock.async_create_device.reset_mock() + # Calling internal function directly to skip trying to time 2 SSDP messages carefully + with caplog.at_level(logging.DEBUG): + await disconnected_source_mock.device_connect() + assert "Not connecting because location is not known" == caplog.records[-1].message + assert not upnp_factory_mock.async_create_device.await_count + + +async def test_become_unavailable( + hass: HomeAssistant, + connected_source_mock: DmsDeviceSource, + dms_device_mock: Mock, +) -> None: + """Test a device becoming unavailable.""" + # Mock a good resolve result + dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( + id="object_id", + restricted=False, + title="Object", + res=[didl_lite.Resource(uri="foo", protocol_info="http-get:*:audio/mpeg:")], + ) + + # Check async_resolve_object currently works + await connected_source_mock.async_resolve_media(":object_id") + + # Now break the network connection + dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError + + # The device should be considered available until next contacted + assert connected_source_mock.available + + # async_resolve_object should fail + with pytest.raises(Unresolvable): + await connected_source_mock.async_resolve_media(":object_id") + + # The device should now be unavailable + assert not connected_source_mock.available diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py new file mode 100644 index 0000000000000..d6fcdb267d6c5 --- /dev/null +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -0,0 +1,953 @@ +"""Test the interface methods of DmsDeviceSource, except availability.""" +from collections.abc import AsyncIterable +from typing import Final, Union +from unittest.mock import ANY, Mock, call + +from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError +from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice +from didl_lite import didl_lite +import pytest + +from homeassistant.components.dlna_dms.const import DLNA_SORT_CRITERIA, DOMAIN +from homeassistant.components.dlna_dms.dms import ( + ActionError, + DeviceConnectionError, + DlnaDmsData, + DmsDeviceSource, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import BrowseMediaSource +from homeassistant.const import CONF_DEVICE_ID, CONF_URL +from homeassistant.core import HomeAssistant + +from .conftest import ( + MOCK_DEVICE_BASE_URL, + MOCK_DEVICE_NAME, + MOCK_DEVICE_TYPE, + MOCK_DEVICE_USN, + MOCK_SOURCE_ID, +) + +from tests.common import MockConfigEntry + +BrowseResultList = list[Union[didl_lite.DidlObject, didl_lite.Descriptor]] + + +@pytest.fixture +async def device_source_mock( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dms_device_mock: Mock, + domain_data_mock: DlnaDmsData, +) -> AsyncIterable[DmsDeviceSource]: + """Fixture to set up a DmsDeviceSource in a connected state and cleanup at completion.""" + await hass.config_entries.async_add(config_entry_mock) + await hass.async_block_till_done() + + mock_entity = domain_data_mock.devices[MOCK_DEVICE_USN] + + # Check the DmsDeviceSource has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + + # Run the test + yield mock_entity + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Check DmsDeviceSource has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + assert MOCK_DEVICE_USN not in domain_data_mock.devices + assert MOCK_SOURCE_ID not in domain_data_mock.sources + + +async def test_update_source_id( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + device_source_mock: DmsDeviceSource, + domain_data_mock: DlnaDmsData, +) -> None: + """Test the config listener updates the source_id and source list upon title change.""" + new_title: Final = "New Name" + new_source_id: Final = "new_name" + assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID} + hass.config_entries.async_update_entry(config_entry_mock, title=new_title) + await hass.async_block_till_done() + + assert device_source_mock.source_id == new_source_id + assert domain_data_mock.sources.keys() == {new_source_id} + + +async def test_update_existing_source_id( + caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + device_source_mock: DmsDeviceSource, + domain_data_mock: DlnaDmsData, +) -> None: + """Test the config listener gracefully handles colliding source_id.""" + new_title: Final = "New Name" + new_source_id: Final = "new_name" + new_source_id_2: Final = "new_name_1" + # Set up another config entry to collide with the new source_id + colliding_entry = MockConfigEntry( + unique_id=f"different-udn::{MOCK_DEVICE_TYPE}", + domain=DOMAIN, + data={ + CONF_URL: "http://192.88.99.22/dms_description.xml", + CONF_DEVICE_ID: f"different-udn::{MOCK_DEVICE_TYPE}", + }, + title=new_title, + ) + await hass.config_entries.async_add(colliding_entry) + await hass.async_block_till_done() + + assert device_source_mock.source_id == MOCK_SOURCE_ID + assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID, new_source_id} + assert domain_data_mock.sources[MOCK_SOURCE_ID] is device_source_mock + + # Update the existing entry to match the other entry's name + hass.config_entries.async_update_entry(config_entry_mock, title=new_title) + await hass.async_block_till_done() + + # The existing device's source ID should be a newly generated slug + assert device_source_mock.source_id == new_source_id_2 + assert domain_data_mock.sources.keys() == {new_source_id, new_source_id_2} + assert domain_data_mock.sources[new_source_id_2] is device_source_mock + + # Changing back to the old name should not cause issues + hass.config_entries.async_update_entry(config_entry_mock, title=MOCK_DEVICE_NAME) + await hass.async_block_till_done() + + assert device_source_mock.source_id == MOCK_SOURCE_ID + assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID, new_source_id} + assert domain_data_mock.sources[MOCK_SOURCE_ID] is device_source_mock + + # Remove the collision and try again + await hass.config_entries.async_remove(colliding_entry.entry_id) + assert domain_data_mock.sources.keys() == {MOCK_SOURCE_ID} + + hass.config_entries.async_update_entry(config_entry_mock, title=new_title) + await hass.async_block_till_done() + + assert device_source_mock.source_id == new_source_id + assert domain_data_mock.sources.keys() == {new_source_id} + + +async def test_catch_request_error_unavailable( + device_source_mock: DmsDeviceSource, +) -> None: + """Test the device is checked for availability before trying requests.""" + device_source_mock._device = None + + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_resolve_object("id") + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_resolve_path("path") + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_resolve_search("query") + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_browse_object("object_id") + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_browse_search("query") + + +async def test_catch_request_error( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test errors when making requests to the device are handled.""" + dms_device_mock.async_browse_metadata.side_effect = UpnpActionError( + error_code=ContentDirectoryErrorCode.NO_SUCH_OBJECT + ) + with pytest.raises(ActionError, match="No such object: bad_id"): + await device_source_mock.async_resolve_media(":bad_id") + + dms_device_mock.async_search_directory.side_effect = UpnpActionError( + error_code=ContentDirectoryErrorCode.INVALID_SEARCH_CRITERIA + ) + with pytest.raises(ActionError, match="Invalid query: bad query"): + await device_source_mock.async_resolve_media("?bad query") + + dms_device_mock.async_browse_metadata.side_effect = UpnpActionError( + error_code=ContentDirectoryErrorCode.CANNOT_PROCESS_REQUEST + ) + with pytest.raises(DeviceConnectionError, match="Server failure: "): + await device_source_mock.async_resolve_media(":good_id") + + dms_device_mock.async_browse_metadata.side_effect = UpnpError + with pytest.raises( + DeviceConnectionError, match="Server communication failure: UpnpError(.*)" + ): + await device_source_mock.async_resolve_media(":bad_id") + + # UpnpConnectionErrors will cause the device_source_mock to disconnect from the device + assert device_source_mock.available + dms_device_mock.async_browse_metadata.side_effect = UpnpConnectionError + with pytest.raises( + DeviceConnectionError, match="Server disconnected: UpnpConnectionError(.*)" + ): + await device_source_mock.async_resolve_media(":bad_id") + assert not device_source_mock.available + + +async def test_icon(device_source_mock: DmsDeviceSource, dms_device_mock: Mock) -> None: + """Test the device's icon URL is returned.""" + assert device_source_mock.icon == dms_device_mock.icon + + device_source_mock._device = None + assert device_source_mock.icon is None + + +async def test_resolve_media_invalid(device_source_mock: DmsDeviceSource) -> None: + """Test async_resolve_media will raise Unresolvable when an identifier isn't supplied.""" + with pytest.raises(Unresolvable, match="Invalid identifier.*"): + await device_source_mock.async_resolve_media("") + + +async def test_resolve_media_object( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test the async_resolve_object method via async_resolve_media.""" + object_id: Final = "123" + res_url: Final = "foo/bar" + res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" + res_mime: Final = "audio/mpeg" + # Success case: one resource + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + result = await device_source_mock.async_resolve_media(f":{object_id}") + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + object_id, metadata_filter="*" + ) + assert result.url == res_abs_url + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + + # Success case: two resources, first is playable + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[ + didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:"), + didl_lite.Resource( + uri="thumbnail.png", protocol_info="http-get:*:image/png:" + ), + ], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + result = await device_source_mock.async_resolve_media(f":{object_id}") + assert result.url == res_abs_url + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + + # Success case: three resources, only third is playable + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[ + didl_lite.Resource(uri="", protocol_info=""), + didl_lite.Resource(uri="internal:thing", protocol_info="internal:*::"), + didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:"), + ], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + result = await device_source_mock.async_resolve_media(f":{object_id}") + assert result.url == res_abs_url + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + + # Failure case: no resources + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + with pytest.raises(Unresolvable, match="Object has no resources"): + await device_source_mock.async_resolve_media(f":{object_id}") + + # Failure case: resources are not playable + didl_item = didl_lite.Item( + id=object_id, + restricted="false", + title="Object", + res=[didl_lite.Resource(uri="internal:thing", protocol_info="internal:*::")], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + with pytest.raises(Unresolvable, match="Object has no playable resources"): + await device_source_mock.async_resolve_media(f":{object_id}") + + +async def test_resolve_media_path( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test the async_resolve_path method via async_resolve_media.""" + path: Final = "path/to/thing" + object_ids: Final = ["path_id", "to_id", "thing_id"] + res_url: Final = "foo/bar" + res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" + res_mime: Final = "audio/mpeg" + + search_directory_result = [] + for ob_id, ob_title in zip(object_ids, path.split("/")): + didl_item = didl_lite.Item( + id=ob_id, + restricted="false", + title=ob_title, + res=[], + ) + search_directory_result.append(DmsDevice.BrowseResult([didl_item], 1, 1, 0)) + + # Test that path is resolved correctly + dms_device_mock.async_search_directory.side_effect = search_directory_result + dms_device_mock.async_browse_metadata.return_value = didl_lite.Item( + id=object_ids[-1], + restricted="false", + title="thing", + res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], + ) + result = await device_source_mock.async_resolve_media(f"/{path}") + assert dms_device_mock.async_search_directory.await_args_list == [ + call( + parent_id, + search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ) + for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + ] + assert result.url == res_abs_url + assert result.mime_type == res_mime + + # Test a path starting with a / (first / is path action, second / is root of path) + dms_device_mock.async_search_directory.reset_mock() + dms_device_mock.async_search_directory.side_effect = search_directory_result + result = await device_source_mock.async_resolve_media(f"//{path}") + assert dms_device_mock.async_search_directory.await_args_list == [ + call( + parent_id, + search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ) + for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + ] + assert result.url == res_abs_url + assert result.mime_type == res_mime + + +async def test_resolve_path_simple( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path for simple success as for test_resolve_media_path.""" + path: Final = "path/to/thing" + object_ids: Final = ["path_id", "to_id", "thing_id"] + search_directory_result = [] + for ob_id, ob_title in zip(object_ids, path.split("/")): + didl_item = didl_lite.Item( + id=ob_id, + restricted="false", + title=ob_title, + res=[], + ) + search_directory_result.append(DmsDevice.BrowseResult([didl_item], 1, 1, 0)) + + dms_device_mock.async_search_directory.side_effect = search_directory_result + result = await device_source_mock.async_resolve_path(path) + assert dms_device_mock.async_search_directory.call_args_list == [ + call( + parent_id, + search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ) + for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + ] + assert result == object_ids[-1] + assert not dms_device_mock.async_browse_direct_children.await_count + + +async def test_resolve_path_browsed( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path: action error results in browsing.""" + path: Final = "path/to/thing" + object_ids: Final = ["path_id", "to_id", "thing_id"] + + search_directory_result = [] + for ob_id, ob_title in zip(object_ids, path.split("/")): + didl_item = didl_lite.Item( + id=ob_id, + restricted="false", + title=ob_title, + res=[], + ) + search_directory_result.append(DmsDevice.BrowseResult([didl_item], 1, 1, 0)) + dms_device_mock.async_search_directory.side_effect = [ + search_directory_result[0], + # 2nd level can't be searched (this happens with Kodi) + UpnpActionError(), + search_directory_result[2], + ] + + browse_children_result: BrowseResultList = [] + for title in ("Irrelevant", "to", "Ignored"): + browse_children_result.append( + didl_lite.Item(id=f"{title}_id", restricted="false", title=title, res=[]) + ) + dms_device_mock.async_browse_direct_children.side_effect = [ + DmsDevice.BrowseResult(browse_children_result, 3, 3, 0) + ] + + result = await device_source_mock.async_resolve_path(path) + # All levels should have an attempted search + assert dms_device_mock.async_search_directory.await_args_list == [ + call( + parent_id, + search_criteria=f'@parentID="{parent_id}" and dc:title="{title}"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ) + for parent_id, title in zip(["0"] + object_ids[:-1], path.split("/")) + ] + assert result == object_ids[-1] + # 2nd level should also be browsed + assert dms_device_mock.async_browse_direct_children.await_args_list == [ + call("path_id", metadata_filter=["id", "upnp:class", "dc:title"]) + ] + + +async def test_resolve_path_browsed_nothing( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path: action error results in browsing, but nothing found.""" + dms_device_mock.async_search_directory.side_effect = UpnpActionError() + # No children + dms_device_mock.async_browse_direct_children.side_effect = [ + DmsDevice.BrowseResult([], 0, 0, 0) + ] + with pytest.raises(Unresolvable, match="No contents for thing in thing/other"): + await device_source_mock.async_resolve_path(r"thing/other") + + # There are children, but they don't match + dms_device_mock.async_browse_direct_children.side_effect = [ + DmsDevice.BrowseResult( + [ + didl_lite.Item( + id="nothingid", restricted="false", title="not thing", res=[] + ) + ], + 1, + 1, + 0, + ) + ] + with pytest.raises(Unresolvable, match="Nothing found for thing in thing/other"): + await device_source_mock.async_resolve_path(r"thing/other") + + +async def test_resolve_path_quoted( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path: quotes and backslashes in the path get escaped correctly.""" + dms_device_mock.async_search_directory.side_effect = [ + DmsDevice.BrowseResult( + [ + didl_lite.Item( + id=r'id_with quote" and back\slash', + restricted="false", + title="path", + res=[], + ) + ], + 1, + 1, + 0, + ), + UpnpError("Quick abort"), + ] + with pytest.raises(DeviceConnectionError): + await device_source_mock.async_resolve_path(r'path/quote"back\slash') + assert dms_device_mock.async_search_directory.await_args_list == [ + call( + "0", + search_criteria='@parentID="0" and dc:title="path"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ), + call( + r'id_with quote" and back\slash', + search_criteria=r'@parentID="id_with quote\" and back\\slash" and dc:title="quote\"back\\slash"', + metadata_filter=["id", "upnp:class", "dc:title"], + requested_count=1, + ), + ] + + +async def test_resolve_path_ambiguous( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path: ambiguous results (too many matches) gives error.""" + dms_device_mock.async_search_directory.side_effect = [ + DmsDevice.BrowseResult( + [ + didl_lite.Item( + id=r"thing 1", + restricted="false", + title="thing", + res=[], + ), + didl_lite.Item( + id=r"thing 2", + restricted="false", + title="thing", + res=[], + ), + ], + 2, + 2, + 0, + ) + ] + with pytest.raises( + Unresolvable, match="Too many items found for thing in thing/other" + ): + await device_source_mock.async_resolve_path(r"thing/other") + + +async def test_resolve_path_no_such_container( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_resolve_path: Explicit check for NO_SUCH_CONTAINER.""" + dms_device_mock.async_search_directory.side_effect = UpnpActionError( + error_code=ContentDirectoryErrorCode.NO_SUCH_CONTAINER + ) + with pytest.raises(Unresolvable, match="No such container: 0"): + await device_source_mock.async_resolve_path(r"thing/other") + + +async def test_resolve_media_search( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test the async_resolve_search method via async_resolve_media.""" + res_url: Final = "foo/bar" + res_abs_url: Final = f"{MOCK_DEVICE_BASE_URL}/{res_url}" + res_mime: Final = "audio/mpeg" + + # No results + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + with pytest.raises(Unresolvable, match='Nothing found for dc:title="thing"'): + await device_source_mock.async_resolve_media('?dc:title="thing"') + assert dms_device_mock.async_search_directory.await_args_list == [ + call( + container_id="0", + search_criteria='dc:title="thing"', + metadata_filter="*", + requested_count=1, + ) + ] + + # One result + dms_device_mock.async_search_directory.reset_mock() + didl_item = didl_lite.Item( + id="thing's id", + restricted="false", + title="thing", + res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], + ) + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [didl_item], 1, 1, 0 + ) + result = await device_source_mock.async_resolve_media('?dc:title="thing"') + assert result.url == res_abs_url + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + assert dms_device_mock.async_search_directory.await_count == 1 + # Values should be taken from search result, not querying the item's metadata + assert dms_device_mock.async_browse_metadata.await_count == 0 + + # Two results - uses the first + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [didl_item], 1, 2, 0 + ) + result = await device_source_mock.async_resolve_media('?dc:title="thing"') + assert result.url == res_abs_url + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + + # Bad result + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [didl_lite.Descriptor("id", "namespace")], 1, 1, 0 + ) + with pytest.raises(Unresolvable, match="Descriptor.* is not a DidlObject"): + await device_source_mock.async_resolve_media('?dc:title="thing"') + + +async def test_browse_media_root( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_browse_media with no identifier will browse the root of the device.""" + dms_device_mock.async_browse_metadata.return_value = didl_lite.DidlObject( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + # No identifier (first opened in media browser) + result = await device_source_mock.async_browse_media(None) + assert result.identifier == f"{MOCK_SOURCE_ID}/:0" + assert result.title == MOCK_DEVICE_NAME + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "0", metadata_filter=ANY + ) + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + "0", metadata_filter=ANY, sort_criteria=ANY + ) + + dms_device_mock.async_browse_metadata.reset_mock() + dms_device_mock.async_browse_direct_children.reset_mock() + # Empty string identifier + result = await device_source_mock.async_browse_media("") + assert result.identifier == f"{MOCK_SOURCE_ID}/:0" + assert result.title == MOCK_DEVICE_NAME + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "0", metadata_filter=ANY + ) + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + "0", metadata_filter=ANY, sort_criteria=ANY + ) + + +async def test_browse_media_object( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_browse_object via async_browse_media.""" + object_id = "1234" + child_titles = ("Item 1", "Thing", "Item 2") + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id=object_id, restricted="false", title="subcontainer" + ) + children_result = DmsDevice.BrowseResult([], 3, 3, 0) + for title in child_titles: + children_result.result.append( + didl_lite.Item( + id=title + "_id", + restricted="false", + title=title, + res=[ + didl_lite.Resource( + uri=title + "_url", protocol_info="http-get:*:audio/mpeg:" + ) + ], + ), + ) + dms_device_mock.async_browse_direct_children.return_value = children_result + + result = await device_source_mock.async_browse_media(f":{object_id}") + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + object_id, metadata_filter=ANY + ) + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=ANY + ) + + assert result.domain == DOMAIN + assert result.identifier == f"{MOCK_SOURCE_ID}/:{object_id}" + assert result.title == "subcontainer" + assert not result.can_play + assert result.can_expand + assert result.children + for child, title in zip(result.children, child_titles): + assert isinstance(child, BrowseMediaSource) + assert child.identifier == f"{MOCK_SOURCE_ID}/:{title}_id" + assert child.title == title + assert child.can_play + assert not child.can_expand + assert not child.children + + +async def test_browse_object_sort_anything( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sort criteria for children where device allows anything.""" + dms_device_mock.sort_capabilities = ["*"] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_superset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows superset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:originalTrackNumber", + "upnp:class", + "upnp:artist", + "dc:creator", + "upnp:genre", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_subset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows subset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:class", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be reduced to only those allowed, + # and in the order specified by DLNA_SORT_CRITERIA + expected_criteria = ["+upnp:class", "+dc:title"] + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=expected_criteria + ) + + +async def test_browse_media_path( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_browse_media with a path.""" + title = "folder" + con_id = "123" + container = didl_lite.Container(id=con_id, restricted="false", title=title) + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [container], 1, 1, 0 + ) + dms_device_mock.async_browse_metadata.return_value = container + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + + result = await device_source_mock.async_browse_media(f"{title}") + assert result.identifier == f"{MOCK_SOURCE_ID}/:{con_id}" + assert result.title == title + + dms_device_mock.async_search_directory.assert_awaited_once_with( + "0", + search_criteria=f'@parentID="0" and dc:title="{title}"', + metadata_filter=ANY, + requested_count=1, + ) + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + con_id, metadata_filter=ANY + ) + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + con_id, metadata_filter=ANY, sort_criteria=ANY + ) + + +async def test_browse_media_search( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test async_browse_media with a search query.""" + query = 'dc:title contains "FooBar"' + object_details = (("111", "FooBar baz"), ("432", "Not FooBar"), ("99", "FooBar")) + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [ + didl_lite.DidlObject(id=ob_id, restricted="false", title=title) + for ob_id, title in object_details + ], + 3, + 3, + 0, + ) + # Test that descriptors are skipped + dms_device_mock.async_search_directory.return_value.result.insert( + 1, didl_lite.Descriptor("id", "name_space") + ) + + result = await device_source_mock.async_browse_media(f"?{query}") + assert result.identifier == f"{MOCK_SOURCE_ID}/?{query}" + assert result.title == "Search results" + assert result.children + + for obj, child in zip(object_details, result.children): + assert isinstance(child, BrowseMediaSource) + assert child.identifier == f"{MOCK_SOURCE_ID}/:{obj[0]}" + assert child.title == obj[1] + assert not child.children + + +async def test_browse_search_invalid( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test searching with an invalid query gives a BrowseError.""" + query = "title == FooBar" + dms_device_mock.async_search_directory.side_effect = UpnpActionError( + error_code=ContentDirectoryErrorCode.INVALID_SEARCH_CRITERIA + ) + with pytest.raises(BrowseError, match=f"Invalid query: {query}"): + await device_source_mock.async_browse_media(f"?{query}") + + +async def test_browse_search_no_results( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test a search with no results does not give an error.""" + query = 'dc:title contains "FooBar"' + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + + result = await device_source_mock.async_browse_media(f"?{query}") + assert result.identifier == f"{MOCK_SOURCE_ID}/?{query}" + assert result.title == "Search results" + assert not result.children + + +async def test_thumbnail( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test getting thumbnails URLs for items.""" + # Use browse_search to get multiple items at once for least effort + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + [ + # Thumbnail as albumArtURI property + didl_lite.MusicAlbum( + id="a", + restricted="false", + title="a", + res=[], + album_art_uri="a_thumb.jpg", + ), + # Thumbnail as resource (1st resource is media item, 2nd is missing + # a URI, 3rd is thumbnail) + didl_lite.MusicTrack( + id="b", + restricted="false", + title="b", + res=[ + didl_lite.Resource( + uri="b_track.mp3", protocol_info="http-get:*:audio/mpeg:" + ), + didl_lite.Resource(uri="", protocol_info="internal:*::"), + didl_lite.Resource( + uri="b_thumb.png", protocol_info="http-get:*:image/png:" + ), + ], + ), + # No thumbnail + didl_lite.MusicTrack( + id="c", + restricted="false", + title="c", + res=[ + didl_lite.Resource( + uri="c_track.mp3", protocol_info="http-get:*:audio/mpeg:" + ) + ], + ), + ], + 3, + 3, + 0, + ) + + result = await device_source_mock.async_browse_media("?query") + assert result.children + assert result.children[0].thumbnail == f"{MOCK_DEVICE_BASE_URL}/a_thumb.jpg" + assert result.children[1].thumbnail == f"{MOCK_DEVICE_BASE_URL}/b_thumb.png" + assert result.children[2].thumbnail is None + + +async def test_can_play( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test determination of playability for items.""" + protocol_infos = [ + # No protocol info for resource + ("", True), + # Protocol info is poorly formatted but can play + ("http-get", True), + # Protocol info is poorly formatted and can't play + ("internal", False), + # Protocol is HTTP + ("http-get:*:audio/mpeg", True), + # Protocol is RTSP + ("rtsp-rtp-udp:*:MPA:", True), + # Protocol is something else + ("internal:*:audio/mpeg:", False), + ] + + search_results: BrowseResultList = [] + # No resources + search_results.append(didl_lite.DidlObject(id="", restricted="false", title="")) + search_results.extend( + didl_lite.MusicTrack( + id="", + restricted="false", + title="", + res=[didl_lite.Resource(uri="", protocol_info=protocol_info)], + ) + for protocol_info, _ in protocol_infos + ) + + # Use browse_search to get multiple items at once for least effort + dms_device_mock.async_search_directory.return_value = DmsDevice.BrowseResult( + search_results, len(search_results), len(search_results), 0 + ) + + result = await device_source_mock.async_browse_media("?query") + assert result.children + assert not result.children[0].can_play + for idx, info_can_play in enumerate(protocol_infos): + protocol_info, can_play = info_can_play + assert result.children[idx + 1].can_play is can_play, f"Checked {protocol_info}" diff --git a/tests/components/dlna_dms/test_init.py b/tests/components/dlna_dms/test_init.py new file mode 100644 index 0000000000000..16254adca89fa --- /dev/null +++ b/tests/components/dlna_dms/test_init.py @@ -0,0 +1,59 @@ +"""Test the DLNA DMS component setup, cleanup, and module-level functions.""" + +from unittest.mock import Mock + +from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.dms import DlnaDmsData +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_resource_lifecycle( + hass: HomeAssistant, + domain_data_mock: DlnaDmsData, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dms_device_mock: Mock, +) -> None: + """Test that resources are acquired/released as the entity is setup/unloaded.""" + # Set up the config entry + config_entry_mock.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + # Check the entity is created and working + assert len(domain_data_mock.devices) == 1 + assert len(domain_data_mock.sources) == 1 + entity = next(iter(domain_data_mock.devices.values())) + assert entity.available is True + + # Check update listeners are subscribed + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + + # Check event notifiers are not subscribed - dlna_dms doesn't use them + assert dms_device_mock.async_subscribe_services.await_count == 0 + assert dms_device_mock.async_unsubscribe_services.await_count == 0 + assert dms_device_mock.on_event is None + + # Unload the config entry + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + # Check update listeners are released + assert not config_entry_mock.update_listeners + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + + # Check event notifiers are still not subscribed + assert dms_device_mock.async_subscribe_services.await_count == 0 + assert dms_device_mock.async_unsubscribe_services.await_count == 0 + assert dms_device_mock.on_event is None + + # Check entity is gone + assert not domain_data_mock.devices + assert not domain_data_mock.sources diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py new file mode 100644 index 0000000000000..4b43402ecbdcd --- /dev/null +++ b/tests/components/dlna_dms/test_media_source.py @@ -0,0 +1,255 @@ +"""Tests for dlna_dms.media_source, mostly testing DmsMediaSource.""" +from unittest.mock import ANY, Mock + +from async_upnp_client.exceptions import UpnpError +from didl_lite import didl_lite +import pytest + +from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.dms import DlnaDmsData, DmsDeviceSource +from homeassistant.components.dlna_dms.media_source import ( + DmsMediaSource, + async_get_media_source, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSourceItem, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_URL +from homeassistant.core import HomeAssistant + +from .conftest import ( + MOCK_DEVICE_BASE_URL, + MOCK_DEVICE_NAME, + MOCK_DEVICE_TYPE, + MOCK_DEVICE_USN, + MOCK_SOURCE_ID, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def entity( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + dms_device_mock: Mock, + domain_data_mock: DlnaDmsData, +) -> DmsDeviceSource: + """Fixture to set up a DmsDeviceSource in a connected state and cleanup at completion.""" + await hass.config_entries.async_add(config_entry_mock) + await hass.async_block_till_done() + return domain_data_mock.devices[MOCK_DEVICE_USN] + + +@pytest.fixture +async def dms_source(hass: HomeAssistant, entity: DmsDeviceSource) -> DmsMediaSource: + """Fixture providing a pre-constructed DmsMediaSource with a single device.""" + return DmsMediaSource(hass) + + +async def test_get_media_source(hass: HomeAssistant) -> None: + """Test the async_get_media_source function and DmsMediaSource constructor.""" + source = await async_get_media_source(hass) + assert isinstance(source, DmsMediaSource) + assert source.domain == DOMAIN + + +async def test_resolve_media_unconfigured(hass: HomeAssistant) -> None: + """Test resolve_media without any devices being configured.""" + source = DmsMediaSource(hass) + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + with pytest.raises(Unresolvable, match="No sources have been configured"): + await source.async_resolve_media(item) + + +async def test_resolve_media_bad_identifier( + hass: HomeAssistant, dms_source: DmsMediaSource +) -> None: + """Test trying to resolve an item that has an unresolvable identifier.""" + # Empty identifier + item = MediaSourceItem(hass, DOMAIN, "") + with pytest.raises(Unresolvable, match="No source ID.*"): + await dms_source.async_resolve_media(item) + + # Identifier has media_id but no source_id + item = MediaSourceItem(hass, DOMAIN, "/media_id") + with pytest.raises(Unresolvable, match="No source ID.*"): + await dms_source.async_resolve_media(item) + + # Identifier has source_id but no media_id + item = MediaSourceItem(hass, DOMAIN, "source_id/") + with pytest.raises(Unresolvable, match="No media ID.*"): + await dms_source.async_resolve_media(item) + + # Identifier is missing source_id/media_id separator + item = MediaSourceItem(hass, DOMAIN, "source_id") + with pytest.raises(Unresolvable, match="No media ID.*"): + await dms_source.async_resolve_media(item) + + # Identifier has an unknown source_id + item = MediaSourceItem(hass, DOMAIN, "unknown_source/media_id") + with pytest.raises(Unresolvable, match="Unknown source ID: unknown_source"): + await dms_source.async_resolve_media(item) + + +async def test_resolve_media_success( + hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock +) -> None: + """Test resolving an item via a DmsDeviceSource.""" + object_id = "123" + item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:{object_id}") + + res_url = "foo/bar" + res_mime = "audio/mpeg" + didl_item = didl_lite.Item( + id=object_id, + restricted=False, + title="Object", + res=[didl_lite.Resource(uri=res_url, protocol_info=f"http-get:*:{res_mime}:")], + ) + dms_device_mock.async_browse_metadata.return_value = didl_item + + result = await dms_source.async_resolve_media(item) + assert result.url == f"{MOCK_DEVICE_BASE_URL}/{res_url}" + assert result.mime_type == res_mime + assert result.didl_metadata is didl_item + + +async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: + """Test browse_media without any devices being configured.""" + source = DmsMediaSource(hass) + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + with pytest.raises(BrowseError, match="No sources have been configured"): + await source.async_browse_media(item) + + item = MediaSourceItem(hass, DOMAIN, "") + with pytest.raises(BrowseError, match="No sources have been configured"): + await source.async_browse_media(item) + + +async def test_browse_media_bad_identifier( + hass: HomeAssistant, dms_source: DmsMediaSource +) -> None: + """Test browse_media with a bad source_id.""" + item = MediaSourceItem(hass, DOMAIN, "bad-id/media_id") + with pytest.raises(BrowseError, match="Unknown source ID: bad-id"): + await dms_source.async_browse_media(item) + + +async def test_browse_media_single_source_no_identifier( + hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock +) -> None: + """Test browse_media without a source_id, with a single device registered.""" + # Fast bail-out, mock will be checked after + dms_device_mock.async_browse_metadata.side_effect = UpnpError + + # No source_id nor media_id + item = MediaSourceItem(hass, DOMAIN, "") + with pytest.raises(BrowseError): + await dms_source.async_browse_media(item) + # Mock device should've been browsed for the root directory + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "0", metadata_filter=ANY + ) + + # No source_id but a media_id + item = MediaSourceItem(hass, DOMAIN, "/:media-item-id") + dms_device_mock.async_browse_metadata.reset_mock() + with pytest.raises(BrowseError): + await dms_source.async_browse_media(item) + # Mock device should've been browsed for the root directory + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "media-item-id", metadata_filter=ANY + ) + + +async def test_browse_media_multiple_sources( + hass: HomeAssistant, dms_source: DmsMediaSource, dms_device_mock: Mock +) -> None: + """Test browse_media without a source_id, with multiple devices registered.""" + # Set up a second source + other_source_id = "second_source" + other_source_title = "Second source" + other_config_entry = MockConfigEntry( + unique_id=f"different-udn::{MOCK_DEVICE_TYPE}", + domain=DOMAIN, + data={ + CONF_URL: "http://192.88.99.22/dms_description.xml", + CONF_DEVICE_ID: f"different-udn::{MOCK_DEVICE_TYPE}", + }, + title=other_source_title, + ) + await hass.config_entries.async_add(other_config_entry) + await hass.async_block_till_done() + + # No source_id nor media_id + item = MediaSourceItem(hass, DOMAIN, "") + result = await dms_source.async_browse_media(item) + # Mock device should not have been browsed + assert dms_device_mock.async_browse_metadata.await_count == 0 + # Result will be a list of available devices + assert result.title == "DLNA Servers" + assert result.children + assert isinstance(result.children[0], BrowseMediaSource) + assert result.children[0].identifier == f"{MOCK_SOURCE_ID}/:0" + assert result.children[0].title == MOCK_DEVICE_NAME + assert isinstance(result.children[1], BrowseMediaSource) + assert result.children[1].identifier == f"{other_source_id}/:0" + assert result.children[1].title == other_source_title + + # No source_id but a media_id - will give the exact same list of all devices + item = MediaSourceItem(hass, DOMAIN, "/:media-item-id") + result = await dms_source.async_browse_media(item) + # Mock device should not have been browsed + assert dms_device_mock.async_browse_metadata.await_count == 0 + # Result will be a list of available devices + assert result.title == "DLNA Servers" + assert result.children + assert isinstance(result.children[0], BrowseMediaSource) + assert result.children[0].identifier == f"{MOCK_SOURCE_ID}/:0" + assert result.children[0].title == MOCK_DEVICE_NAME + assert isinstance(result.children[1], BrowseMediaSource) + assert result.children[1].identifier == f"{other_source_id}/:0" + assert result.children[1].title == other_source_title + + +async def test_browse_media_source_id( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + dms_device_mock: Mock, + domain_data_mock: DlnaDmsData, +) -> None: + """Test browse_media with an explicit source_id.""" + # Set up a second device first, then the primary mock device. + # This allows testing that the right source is chosen by source_id + other_source_title = "Second source" + other_config_entry = MockConfigEntry( + unique_id=f"different-udn::{MOCK_DEVICE_TYPE}", + domain=DOMAIN, + data={ + CONF_URL: "http://192.88.99.22/dms_description.xml", + CONF_DEVICE_ID: f"different-udn::{MOCK_DEVICE_TYPE}", + }, + title=other_source_title, + ) + await hass.config_entries.async_add(other_config_entry) + await hass.async_block_till_done() + + await hass.config_entries.async_add(config_entry_mock) + await hass.async_block_till_done() + + # Fast bail-out, mock will be checked after + dms_device_mock.async_browse_metadata.side_effect = UpnpError + + # Browse by source_id + item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:media-item-id") + dms_source = DmsMediaSource(hass) + with pytest.raises(BrowseError): + await dms_source.async_browse_media(item) + # Mock device should've been browsed for the root directory + dms_device_mock.async_browse_metadata.assert_awaited_once_with( + "media-item-id", metadata_filter=ANY + ) diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index ccb05b327ac56..10cc4a4fb778b 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -84,6 +84,7 @@ async def test_form_zeroconf_wrong_oui(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.8", + addresses=["192.168.1.8"], hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", port=None, @@ -103,6 +104,7 @@ async def test_form_zeroconf_link_local_ignored(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="169.254.103.61", + addresses=["169.254.103.61"], hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", port=None, @@ -129,6 +131,7 @@ async def test_form_zeroconf_correct_oui(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.5", + addresses=["192.168.1.5"], hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", port=None, @@ -191,6 +194,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.5", + addresses=["192.168.1.5"], hostname="mock_hostname", name="Doorstation - abc123._axis-video._tcp.local.", port=None, diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 692870d70374f..669fcfac386b5 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DSMR config flow.""" +import asyncio from itertools import chain, repeat import os from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch, sentinel @@ -138,6 +139,45 @@ async def test_setup_5L(com_mock, hass, dsmr_connection_send_validate_fixture): assert result["data"] == entry_data +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_5S(com_mock, hass, dsmr_connection_send_validate_fixture): + """Test we can setup serial.""" + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": port.device, "dsmr_version": "5S"} + ) + + entry_data = { + "port": port.device, + "dsmr_version": "5S", + "serial_id": None, + "serial_id_gas": None, + } + + assert result["type"] == "create_entry" + assert result["title"] == port.device + assert result["data"] == entry_data + + @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_Q3D(com_mock, hass, dsmr_connection_send_validate_fixture): """Test we can setup serial.""" @@ -265,6 +305,48 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f assert result["errors"] == {"base": "cannot_connect"} +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_timeout( + com_mock, hass, dsmr_connection_send_validate_fixture +): + """Test failed serial connection.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + first_timeout_wait_closed = AsyncMock( + return_value=True, + side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + ) + protocol.wait_closed = first_timeout_wait_closed + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {"base": "cannot_communicate"} + + @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_wrong_telegram( com_mock, hass, dsmr_connection_send_validate_fixture diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index 4c26e25e5f48d..ddddc56f4e4a6 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -57,9 +57,9 @@ async def mock_responses( """Mock responses from Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" api = Efergy( - token, session=async_get_clientsession(hass), utc_offset=hass.config.time_zone + token, session=async_get_clientsession(hass), utc_offset="America/New_York" ) - offset = api._utc_offset # pylint: disable=protected-access + assert api._utc_offset == 300 if error: aioclient_mock.get( f"{base_url}getInstant?token={token}", @@ -75,19 +75,19 @@ async def mock_responses( text=load_fixture("efergy/instant.json"), ) aioclient_mock.get( - f"{base_url}getEnergy?token={token}&offset={offset}&period=day", + f"{base_url}getEnergy?period=day", text=load_fixture("efergy/daily_energy.json"), ) aioclient_mock.get( - f"{base_url}getEnergy?token={token}&offset={offset}&period=week", + f"{base_url}getEnergy?period=week", text=load_fixture("efergy/weekly_energy.json"), ) aioclient_mock.get( - f"{base_url}getEnergy?token={token}&offset={offset}&period=month", + f"{base_url}getEnergy?period=month", text=load_fixture("efergy/monthly_energy.json"), ) aioclient_mock.get( - f"{base_url}getEnergy?token={token}&offset={offset}&period=year", + f"{base_url}getEnergy?period=year", text=load_fixture("efergy/yearly_energy.json"), ) aioclient_mock.get( @@ -95,19 +95,19 @@ async def mock_responses( text=load_fixture("efergy/budget.json"), ) aioclient_mock.get( - f"{base_url}getCost?token={token}&offset={offset}&period=day", + f"{base_url}getCost?period=day", text=load_fixture("efergy/daily_cost.json"), ) aioclient_mock.get( - f"{base_url}getCost?token={token}&offset={offset}&period=week", + f"{base_url}getCost?period=week", text=load_fixture("efergy/weekly_cost.json"), ) aioclient_mock.get( - f"{base_url}getCost?token={token}&offset={offset}&period=month", + f"{base_url}getCost?period=month", text=load_fixture("efergy/monthly_cost.json"), ) aioclient_mock.get( - f"{base_url}getCost?token={token}&offset={offset}&period=year", + f"{base_url}getCost?period=year", text=load_fixture("efergy/yearly_cost.json"), ) if token == TOKEN: diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index dffd59cedcc45..fdc0ad834d13d 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -61,6 +61,7 @@ async def test_full_zeroconf_flow_implementation( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="example.local.", name="mock_name", port=9123, @@ -126,6 +127,7 @@ async def test_zeroconf_connection_error( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=9123, @@ -167,6 +169,7 @@ async def test_zeroconf_device_exists_abort( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=9123, @@ -187,6 +190,7 @@ async def test_zeroconf_device_exists_abort( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="127.0.0.2", + addresses=["127.0.0.2"], hostname="mock_hostname", name="mock_name", port=9123, diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py index 8ae7f6d7b49eb..128d0a0d77764 100644 --- a/tests/components/elkm1/__init__.py +++ b/tests/components/elkm1/__init__.py @@ -1 +1,61 @@ """Tests for the Elk-M1 Control integration.""" + +from contextlib import contextmanager +from unittest.mock import MagicMock, patch + +from elkm1_lib.discovery import ElkSystem + +MOCK_IP_ADDRESS = "127.0.0.1" +MOCK_MAC = "aa:bb:cc:dd:ee:ff" +ELK_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2601) +ELK_NON_SECURE_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2101) + + +def mock_elk(invalid_auth=None, sync_complete=None, exception=None): + """Mock m1lib Elk.""" + + def handler_callbacks(type_, callback): + nonlocal invalid_auth, sync_complete + if exception: + raise exception + if type_ == "login": + callback(not invalid_auth) + elif type_ == "sync_complete" and sync_complete: + callback() + + mocked_elk = MagicMock() + mocked_elk.add_handler.side_effect = handler_callbacks + return mocked_elk + + +def _patch_discovery(device=None, no_device=False): + async def _discovery(*args, **kwargs): + return [] if no_device else [device or ELK_DISCOVERY] + + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.elkm1.discovery.AIOELKDiscovery.async_scan", + new=_discovery, + ): + yield + + return _patcher() + + +def _patch_elk(elk=None): + def _elk(*args, **kwargs): + return elk if elk else mock_elk() + + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + new=_elk, + ), patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + new=_elk, + ): + yield + + return _patcher() diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index ab5ebba79ebe0..49402d7b4d51d 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,43 +1,48 @@ """Test the Elk-M1 Control config flow.""" +from dataclasses import asdict +from unittest.mock import patch -from unittest.mock import MagicMock, patch +import pytest from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.elkm1.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from . import ( + ELK_DISCOVERY, + ELK_NON_SECURE_DISCOVERY, + MOCK_IP_ADDRESS, + MOCK_MAC, + _patch_discovery, + _patch_elk, + mock_elk, +) -def mock_elk(invalid_auth=None, sync_complete=None): - """Mock m1lib Elk.""" +from tests.common import MockConfigEntry - def handler_callbacks(type_, callback): - nonlocal invalid_auth, sync_complete +DHCP_DISCOVERY = dhcp.DhcpServiceInfo(MOCK_IP_ADDRESS, "", MOCK_MAC) +ELK_DISCOVERY_INFO = asdict(ELK_DISCOVERY) +MODULE = "homeassistant.components.elkm1" - if type_ == "login": - if invalid_auth is not None: - callback(not invalid_auth) - elif type_ == "sync_complete" and sync_complete: - callback() - mocked_elk = MagicMock() - mocked_elk.add_handler.side_effect = handler_callbacks - return mocked_elk - - -async def test_form_user_with_secure_elk(hass): +async def test_form_user_with_secure_elk_no_discovery(hass): """Test we can setup a secure elk.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" assert result["errors"] == {} + assert result["step_id"] == "manual_connection" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", - return_value=mocked_elk, - ), patch( + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.elkm1.async_setup_entry", @@ -50,7 +55,6 @@ async def test_form_user_with_secure_elk(hass): "address": "1.2.3.4", "username": "test-username", "password": "test-password", - "temperature_unit": "°F", "prefix": "", }, ) @@ -63,28 +67,227 @@ async def test_form_user_with_secure_elk(hass): "host": "elks://1.2.3.4", "password": "test-password", "prefix": "", - "temperature_unit": "°F", "username": "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_user_with_tls_elk(hass): +async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass): + """Test we abort when we try to configure the same ip.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "127.0.0.1", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "address_already_configured" + + +async def test_form_user_with_secure_elk_with_discovery(hass): """Test we can setup a secure elk.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": MOCK_MAC}, + ) + await hass.async_block_till_done() + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:2601", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert result3["result"].unique_id == "aa:bb:cc:dd:ee:ff" + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_secure_elk_with_discovery_pick_manual(hass): + """Test we can setup a secure elk with discovery but user picks manual and directed discovery fails.""" + + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": None}, + ) + await hass.async_block_till_done() + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://1.2.3.4", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert result3["result"].unique_id is None + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_discovery( + hass, +): + """Test we can setup a secure elk with discovery but user picks manual and directed discovery succeeds.""" + + with _patch_discovery(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": None}, + ) + await hass.async_block_till_done() + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "secure", + "address": "127.0.0.1", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:2601", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert result3["result"].unique_id == MOCK_MAC + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_tls_elk_no_discovery(hass): + """Test we can setup a secure elk.""" + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" assert result["errors"] == {} + assert result["step_id"] == "manual_connection" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", - return_value=mocked_elk, - ), patch( + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.elkm1.async_setup_entry", @@ -97,7 +300,6 @@ async def test_form_user_with_tls_elk(hass): "address": "1.2.3.4", "username": "test-username", "password": "test-password", - "temperature_unit": "°F", "prefix": "", }, ) @@ -110,28 +312,28 @@ async def test_form_user_with_tls_elk(hass): "host": "elksv1_2://1.2.3.4", "password": "test-password", "prefix": "", - "temperature_unit": "°F", "username": "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_user_with_non_secure_elk(hass): +async def test_form_user_with_non_secure_elk_no_discovery(hass): """Test we can setup a non-secure elk.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" assert result["errors"] == {} + assert result["step_id"] == "manual_connection" mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) - with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", - return_value=mocked_elk, - ), patch( + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.elkm1.async_setup_entry", @@ -142,7 +344,6 @@ async def test_form_user_with_non_secure_elk(hass): { "protocol": "non-secure", "address": "1.2.3.4", - "temperature_unit": "°F", "prefix": "guest_house", }, ) @@ -156,27 +357,27 @@ async def test_form_user_with_non_secure_elk(hass): "prefix": "guest_house", "username": "", "password": "", - "temperature_unit": "°F", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_user_with_serial_elk(hass): +async def test_form_user_with_serial_elk_no_discovery(hass): """Test we can setup a serial elk.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" assert result["errors"] == {} + assert result["step_id"] == "manual_connection" mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) - with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", - return_value=mocked_elk, - ), patch( + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.elkm1.async_setup_entry", @@ -187,7 +388,6 @@ async def test_form_user_with_serial_elk(hass): { "protocol": "serial", "address": "/dev/ttyS0:115200", - "temperature_unit": "°C", "prefix": "", }, ) @@ -201,7 +401,6 @@ async def test_form_user_with_serial_elk(hass): "prefix": "", "username": "", "password": "", - "temperature_unit": "°C", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -209,18 +408,50 @@ async def test_form_user_with_serial_elk(hass): async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) mocked_elk = mock_elk(invalid_auth=None, sync_complete=None) - with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", - return_value=mocked_elk, + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, ), patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", + 0, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_unknown_exception(hass): + """Test we handle an unknown exception during connecting.""" + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mocked_elk = mock_elk(invalid_auth=None, sync_complete=None, exception=OSError) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", 0, + ), patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", + 0, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -229,13 +460,12 @@ async def test_form_cannot_connect(hass): "address": "1.2.3.4", "username": "test-username", "password": "test-password", - "temperature_unit": "°F", "prefix": "", }, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "unknown"} async def test_form_invalid_auth(hass): @@ -257,23 +487,46 @@ async def test_form_invalid_auth(hass): "address": "1.2.3.4", "username": "test-username", "password": "test-password", - "temperature_unit": "°F", "prefix": "", }, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} -async def test_form_import(hass): - """Test we get the form with import source.""" +async def test_form_invalid_auth_no_password(hass): + """Test we handle invalid auth error when no password is provided.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mocked_elk = mock_elk(invalid_auth=True, sync_complete=True) - mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, - ), patch( + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "", + "prefix": "", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} + + +async def test_form_import(hass): + """Test we get the form with import source.""" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.elkm1.async_setup_entry", @@ -332,3 +585,425 @@ async def test_form_import(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_device_discovered(hass): + """Test we can import with discovery.""" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "elks://127.0.0.1", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "ohana" + assert result["result"].unique_id == MOCK_MAC + assert result["data"] == { + "auto_configure": False, + "host": "elks://127.0.0.1", + "keypad": {"enabled": True, "exclude": [], "include": [[1, 1], [2, 2], [3, 3]]}, + "output": {"enabled": False, "exclude": [], "include": []}, + "password": "love", + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "temperature_unit": "C", + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "username": "friend", + "zone": {"enabled": True, "exclude": [[15, 15], [28, 208]], "include": []}, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_existing(hass): + """Test we abort on existing import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": f"elks://{MOCK_IP_ADDRESS}", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, ELK_DISCOVERY_INFO), + ], +) +async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already_configured( + hass, source, data +): + """Test we abort if the host is already configured but the mac does not match.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, ELK_DISCOVERY_INFO), + ], +) +async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( + hass, source, data +): + """Test we add a missing unique id to the config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MOCK_MAC + + +async def test_discovered_by_discovery_and_dhcp(hass): + """Test we get the form with discovery and abort for dhcp source when we get both.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=ELK_DISCOVERY_INFO, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with _patch_discovery(), _patch_elk(): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with _patch_discovery(), _patch_elk(): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="any", + ip=MOCK_IP_ADDRESS, + macaddress="00:00:00:00:00:00", + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + +async def test_discovered_by_discovery(hass): + """Test we can setup when discovered from discovery.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=ELK_DISCOVERY_INFO, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1 ddeeff" + assert result2["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:2601", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_discovery_url_already_configured(hass): + """Test we abort when we discover a device that is already setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=ELK_DISCOVERY_INFO, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_discovered_by_dhcp_udp_responds(hass): + """Test we can setup when discovered from dhcp but with udp response.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1 ddeeff" + assert result2["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:2601", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): + """Test we can setup when discovered from dhcp but with udp response using the non-secure port.""" + + with _patch_discovery(device=ELK_NON_SECURE_DISCOVERY), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(device=ELK_NON_SECURE_DISCOVERY), _patch_elk( + elk=mocked_elk + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1 ddeeff" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://127.0.0.1:2101", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): + """Test we can setup when discovered from dhcp but with udp response with an existing config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "elks://6.6.6.6"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1 ddeeff" + assert result2["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:2601", + "password": "test-password", + "prefix": "ddeeff", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_discovered_by_dhcp_no_udp_response(hass): + """Test we can setup when discovered from dhcp but no udp response.""" + + with _patch_discovery(no_device=True), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index d8b23ac9864d0..76179c02e22fe 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -212,6 +212,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="mock_name", port=None, @@ -312,6 +313,7 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="mock_name", port=None, @@ -351,6 +353,7 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 25103c4fe2a3a..f7da5d66bd5e8 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -218,6 +218,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], hostname="test8266.local.", name="mock_name", port=6053, @@ -252,6 +253,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], hostname="test8266.local.", name="mock_name", port=6053, @@ -279,6 +281,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], hostname="test8266.local.", name="mock_name", port=6053, @@ -310,6 +313,7 @@ async def test_discovery_already_configured_name(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.184", + addresses=["192.168.43.184"], hostname="test8266.local.", name="mock_name", port=6053, @@ -331,6 +335,7 @@ async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], hostname="test8266.local.", name="mock_name", port=6053, @@ -364,6 +369,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], hostname="test8266.local.", name="mock_name", port=6053, diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 4dffe1d7e255a..9a3129b7dd6b9 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -19,7 +19,11 @@ DEFAULT_TIMEOUT, DOMAIN, ) -from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_INTEGRATION_DISCOVERY, + SOURCE_USER, +) from homeassistant.const import ( CONF_CUSTOMIZE, CONF_IP_ADDRESS, @@ -175,7 +179,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): """Test discovery and confirm step, abort if cloud account was removed.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO + DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" @@ -194,7 +198,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): assert result["reason"] == "ezviz_cloud_account_missing" -async def test_async_step_discovery( +async def test_async_step_integration_discovery( hass, ezviz_config_flow, ezviz_test_rtsp_config_flow ): """Test discovery and confirm step.""" @@ -202,7 +206,7 @@ async def test_async_step_discovery( await init_integration(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO + DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "confirm" @@ -353,7 +357,7 @@ async def test_discover_exception_step1( result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) assert result["type"] == RESULT_TYPE_FORM @@ -428,7 +432,7 @@ async def test_discover_exception_step3( result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_DISCOVERY}, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 9650c6945a61d..5c91460237eb1 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -4,15 +4,16 @@ import pytest -import homeassistant.components.notify as notify +from homeassistant.components import notify from homeassistant.components.notify import ATTR_TITLE_DEFAULT +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import assert_setup_component -async def test_bad_config(hass): +async def test_bad_config(hass: HomeAssistant): """Test set up the platform with bad/missing config.""" config = {notify.DOMAIN: {"name": "test", "platform": "file"}} with assert_setup_component(0) as handle_config: @@ -27,7 +28,7 @@ async def test_bad_config(hass): True, ], ) -async def test_notify_file(hass, timestamp): +async def test_notify_file(hass: HomeAssistant, timestamp: bool): """Test the notify file output.""" filename = "mock_file" message = "one, two, testing, testing" diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 99e08362ab75a..97fe6250d024e 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -4,6 +4,7 @@ import pytest from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import mock_registry @@ -17,7 +18,7 @@ def entity_reg(hass): @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) -async def test_file_value(hass, entity_reg): +async def test_file_value(hass: HomeAssistant) -> None: """Test the File sensor.""" config = { "sensor": {"platform": "file", "name": "file1", "file_path": "mock.file1"} @@ -36,7 +37,7 @@ async def test_file_value(hass, entity_reg): @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) -async def test_file_value_template(hass, entity_reg): +async def test_file_value_template(hass: HomeAssistant) -> None: """Test the File sensor with JSON entries.""" config = { "sensor": { @@ -47,7 +48,9 @@ async def test_file_value_template(hass, entity_reg): } } - data = '{"temperature": 29, "humidity": 31}\n' '{"temperature": 26, "humidity": 36}' + data = ( + '{"temperature": 29, "humidity": 31}\n' + '{"temperature": 26, "humidity": 36}' + ) m_open = mock_open(read_data=data) with patch( @@ -62,7 +65,7 @@ async def test_file_value_template(hass, entity_reg): @patch("os.path.isfile", Mock(return_value=True)) @patch("os.access", Mock(return_value=True)) -async def test_file_empty(hass, entity_reg): +async def test_file_empty(hass: HomeAssistant) -> None: """Test the File sensor with an empty file.""" config = {"sensor": {"platform": "file", "name": "file3", "file_path": "mock.file"}} @@ -75,3 +78,21 @@ async def test_file_empty(hass, entity_reg): state = hass.states.get("sensor.file3") assert state.state == STATE_UNKNOWN + + +@patch("os.path.isfile", Mock(return_value=True)) +@patch("os.access", Mock(return_value=True)) +async def test_file_path_invalid(hass: HomeAssistant) -> None: + """Test the File sensor with invalid path.""" + config = { + "sensor": {"platform": "file", "name": "file4", "file_path": "mock.file4"} + } + + m_open = mock_open(read_data="43\n45\n21") + with patch( + "homeassistant.components.file.sensor.open", m_open, create=True + ), patch.object(hass.config, "is_allowed_path", return_value=False): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("sensor")) == 0 diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 72d0d112f1752..fa85ce414377f 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -7,7 +7,9 @@ from homeassistant import config as hass_config from homeassistant.components.filesize import DOMAIN from homeassistant.components.filesize.sensor import CONF_FILE_PATHS -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -16,21 +18,21 @@ TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") -def create_file(path): +def create_file(path) -> None: """Create a test file.""" with open(path, "w") as test_file: test_file.write("test") @pytest.fixture(autouse=True) -def remove_file(): +def remove_file() -> None: """Remove test file.""" yield if os.path.isfile(TEST_FILE): os.remove(TEST_FILE) -async def test_invalid_path(hass): +async def test_invalid_path(hass: HomeAssistant) -> None: """Test that an invalid path is caught.""" config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: ["invalid_path"]}} assert await async_setup_component(hass, "sensor", config) @@ -38,7 +40,21 @@ async def test_invalid_path(hass): assert len(hass.states.async_entity_ids("sensor")) == 0 -async def test_valid_path(hass): +async def test_cannot_access_file(hass: HomeAssistant) -> None: + """Test that an invalid path is caught.""" + config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} + + with patch( + "homeassistant.components.filesize.sensor.pathlib", + side_effect=OSError("Can not access"), + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("sensor")) == 0 + + +async def test_valid_path(hass: HomeAssistant) -> None: """Test for a valid path.""" create_file(TEST_FILE) config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} @@ -51,7 +67,34 @@ async def test_valid_path(hass): assert state.attributes.get("bytes") == 4 -async def test_reload(hass, tmpdir): +async def test_state_unknown(hass: HomeAssistant, tmpdir: str) -> None: + """Verify we handle state unavailable.""" + create_file(TEST_FILE) + testfile = f"{tmpdir}/file" + await hass.async_add_executor_job(create_file, testfile) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "filesize", + "file_paths": [testfile], + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.file") + + await hass.async_add_executor_job(os.remove, testfile) + await async_update_entity(hass, "sensor.file") + + state = hass.states.get("sensor.file") + assert state.state == STATE_UNKNOWN + + +async def test_reload(hass: HomeAssistant, tmpdir: str) -> None: """Verify we can reload filesize sensors.""" testfile = f"{tmpdir}/file" await hass.async_add_executor_job(create_file, testfile) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index c2fc8cbdd06b2..b044b2c08fa63 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -26,6 +26,7 @@ STATE_UNKNOWN, ) import homeassistant.core as ha +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -256,6 +257,7 @@ async def test_setup(hass): "sensor": { "platform": "filter", "name": "test", + "unique_id": "uniqueid_sensor_test", "entity_id": "sensor.test_monitored", "filters": [ {"filter": "outlier", "window_size": 10, "radius": 4.0}, @@ -285,6 +287,12 @@ async def test_setup(hass): assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING assert state.state == "1.0" + entity_reg = er.async_get(hass) + entity_id = entity_reg.async_get_entity_id( + "sensor", DOMAIN, "uniqueid_sensor_test" + ) + assert entity_id == "sensor.test" + async def test_invalid_state(hass): """Test if filter attributes are inherited.""" @@ -318,6 +326,37 @@ async def test_invalid_state(hass): assert state.state == STATE_UNAVAILABLE +async def test_timestamp_state(hass): + """Test if filter state is a datetime.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "time_throttle", "window_size": "00:02"}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + "2022-02-01T23:04:05+00:00", + {ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == "2022-02-01T23:04:05+00:00" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + + async def test_outlier(values): """Test if outlier filter works.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) diff --git a/tests/components/fivem/__init__.py b/tests/components/fivem/__init__.py new file mode 100644 index 0000000000000..019f9b829135b --- /dev/null +++ b/tests/components/fivem/__init__.py @@ -0,0 +1 @@ +"""Tests for the FiveM integration.""" diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py new file mode 100644 index 0000000000000..a1a1e8f2a379d --- /dev/null +++ b/tests/components/fivem/test_config_flow.py @@ -0,0 +1,145 @@ +"""Test the FiveM config flow.""" +from unittest.mock import patch + +from fivem import FiveMServerOfflineError + +from homeassistant import config_entries +from homeassistant.components.fivem.config_flow import DEFAULT_PORT +from homeassistant.components.fivem.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +USER_INPUT = { + CONF_HOST: "fivem.dummyserver.com", + CONF_PORT: DEFAULT_PORT, +} + + +def _mock_fivem_info_success(): + return { + "resources": [ + "fivem", + "monitor", + ], + "server": "FXServer-dummy v0.0.0.DUMMY linux", + "vars": { + "gamename": "gta5", + }, + "version": 123456789, + } + + +def _mock_fivem_info_invalid(): + return { + "plugins": [ + "sample", + ], + "data": { + "gamename": "gta5", + }, + } + + +def _mock_fivem_info_invalid_game_name(): + info = _mock_fivem_info_success() + info["vars"]["gamename"] = "redm" + + return info + + +async def test_show_config_form(hass: HomeAssistant) -> None: + """Test if initial configuration form is shown.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +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"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "fivem.fivem.FiveM.get_info_raw", + return_value=_mock_fivem_info_success(), + ), patch( + "homeassistant.components.fivem.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == USER_INPUT[CONF_HOST] + assert result2["data"] == USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "fivem.fivem.FiveM.get_info_raw", + side_effect=FiveMServerOfflineError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "fivem.fivem.FiveM.get_info_raw", + return_value=_mock_fivem_info_invalid(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_invalid_game_name(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "fivem.fivem.FiveM.get_info_raw", + return_value=_mock_fivem_info_invalid_game_name(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/flipr/test_binary_sensor.py b/tests/components/flipr/test_binary_sensor.py index 48f9361723c4f..fc24ddee3405d 100644 --- a/tests/components/flipr/test_binary_sensor.py +++ b/tests/components/flipr/test_binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -36,7 +37,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 7fd04fbc99244..c5ab3dc1541a0 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import patch +from flipr_api.exceptions import FliprError + from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import ( ATTR_ICON, @@ -11,6 +13,7 @@ TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -42,7 +45,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", @@ -84,3 +87,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" assert state.state == "0.23654886" + + +async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: + """Test the Flipr sensors error.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test_entry_unique_id", + data={ + CONF_EMAIL: "toto@toto.com", + CONF_PASSWORD: "myPassword", + CONF_FLIPR_ID: "myfliprid", + }, + ) + + entry.add_to_hass(hass) + + registry = entity_reg.async_get(hass) + + with patch( + "flipr_api.FliprAPIRestClient.get_pool_measure_latest", + side_effect=FliprError("Error during flipr data retrieval..."), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Check entity is not generated because of the FliprError raised. + entity = registry.async_get("sensor.flipr_myfliprid_red_ox") + assert entity is None diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index e1abebd40f1fc..65fb704e4c786 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -110,6 +110,7 @@ async def _save_setup_callback(callback: Callable) -> None: bulb.paired_remotes = 2 bulb.pixels_per_segment = 300 bulb.segments = 2 + bulb.diagnostics = {"mock_diag": "mock_diag"} bulb.music_pixels_per_segment = 150 bulb.music_segments = 4 bulb.operating_mode = "RGB&W" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index dcab5cc01add3..b858f6d995a85 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -23,7 +23,7 @@ TRANSITION_JUMP, TRANSITION_STROBE, ) -from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -94,7 +94,6 @@ async def test_discovery(hass: HomeAssistant): assert result3["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -170,7 +169,6 @@ async def test_discovery_legacy(hass: HomeAssistant): assert result3["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -253,7 +251,6 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant): assert result3["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -330,7 +327,6 @@ async def test_manual_working_discovery(hass: HomeAssistant): assert result4["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -377,7 +373,6 @@ async def test_manual_no_discovery_data(hass: HomeAssistant): CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_DESCRIPTION: MODEL_DESCRIPTION, - CONF_NAME: IP_ADDRESS, } @@ -387,7 +382,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=FLUX_DISCOVERY, ) await hass.async_block_till_done() @@ -425,7 +420,7 @@ async def test_discovered_by_discovery(hass): with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=FLUX_DISCOVERY, ) await hass.async_block_till_done() @@ -445,7 +440,6 @@ async def test_discovered_by_discovery(hass): assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -483,7 +477,6 @@ async def test_discovered_by_dhcp_udp_responds(hass): assert result2["data"] == { CONF_MINOR_VERSION: 4, CONF_HOST: IP_ADDRESS, - CONF_NAME: DEFAULT_ENTRY_TITLE, CONF_MODEL: MODEL, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_INFO: MODEL, @@ -522,7 +515,6 @@ async def test_discovered_by_dhcp_no_udp_response(hass): CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_DESCRIPTION: MODEL_DESCRIPTION, - CONF_NAME: DEFAULT_ENTRY_TITLE, } assert mock_async_setup.called assert mock_async_setup_entry.called @@ -553,7 +545,6 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): CONF_HOST: IP_ADDRESS, CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_DESCRIPTION: MODEL_DESCRIPTION, - CONF_NAME: DEFAULT_ENTRY_TITLE, } assert mock_async_setup.called assert mock_async_setup_entry.called @@ -576,7 +567,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): "source, data", [ (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), - (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY), ], ) async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( @@ -602,7 +593,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( "source, data", [ (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), - (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY), ], ) async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already_configured( @@ -630,7 +621,8 @@ async def test_options(hass: HomeAssistant): """Test options flow.""" config_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + data={CONF_HOST: IP_ADDRESS}, + title=IP_ADDRESS, options={ CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", CONF_CUSTOM_EFFECT_SPEED_PCT: 30, diff --git a/tests/components/flux_led/test_diagnostics.py b/tests/components/flux_led/test_diagnostics.py new file mode 100644 index 0000000000000..c641d88c8e243 --- /dev/null +++ b/tests/components/flux_led/test_diagnostics.py @@ -0,0 +1,40 @@ +"""Test flux_led diagnostics.""" +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.setup import async_setup_component + +from . import ( + _mock_config_entry_for_bulb, + _mocked_bulb, + _patch_discovery, + _patch_wifibulb, +) + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + entry = _mock_config_entry_for_bulb(hass) + bulb = _mocked_bulb() + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "data": {"mock_diag": "mock_diag"}, + "entry": { + "data": { + "host": "127.0.0.1", + "minor_version": 4, + "model": "AK001-ZJ2149", + "model_description": "Bulb RGBCW", + "model_info": "AK001-ZJ2149", + "model_num": 53, + "name": "Bulb RGBCW DDEEFF", + "remote_access_enabled": True, + "remote_access_host": "the.cloud", + "remote_access_port": 8816, + }, + "title": "Mock Title", + }, + } diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index de655c2e6adb7..489f6c932c299 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -117,7 +117,7 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( ) -> None: """Test that the unique id is added if its missing via directed (not broadcast) discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None, title=IP_ADDRESS ) config_entry.add_to_hass(hass) last_address = None @@ -144,7 +144,6 @@ def _mock_getBulbInfo(*args, **kwargs): assert config_entry.state == ConfigEntryState.LOADED assert config_entry.unique_id == MAC_ADDRESS - assert config_entry.data[CONF_NAME] == title assert config_entry.title == title diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index b2a88b00fe06b..91be62e5ab7d6 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -299,3 +299,23 @@ async def test_select_white_channel_type(hass: HomeAssistant) -> None: == WhiteChannelType.NATURAL.name.lower() ) assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_select_device_no_wiring(hass: HomeAssistant) -> None: + """Test select is not created if the device does not support wiring.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.wiring = None + bulb.wirings = ["RGB", "GRB"] + bulb.raw_state = bulb.raw_state._replace(model_num=0x25) + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + wiring_entity_id = "select.bulb_rgbcw_ddeeff_wiring" + assert hass.states.get(wiring_entity_id) is None diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index abd2f1ab3a204..fd4d82b177c76 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -101,6 +101,7 @@ async def test_zeroconf_updates_title(hass, config_entry): assert len(hass.config_entries.async_entries(DOMAIN)) == 2 discovery_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.1", + addresses=["192.168.1.1"], hostname="mock_hostname", name="mock_name", port=23, @@ -135,6 +136,7 @@ async def test_config_flow_zeroconf_invalid(hass): # test with no discovery properties discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=23, @@ -149,6 +151,7 @@ async def test_config_flow_zeroconf_invalid(hass): # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=23, @@ -163,6 +166,7 @@ async def test_config_flow_zeroconf_invalid(hass): # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=23, @@ -177,6 +181,7 @@ async def test_config_flow_zeroconf_invalid(hass): # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="mock_name", port=23, @@ -194,6 +199,7 @@ async def test_config_flow_zeroconf_valid(hass): """Test that a valid zeroconf entry works.""" discovery_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.1", + addresses=["192.168.1.1"], hostname="mock_hostname", name="mock_name", port=23, diff --git a/tests/components/freebox/test_button.py b/tests/components/freebox/test_button.py new file mode 100644 index 0000000000000..0a6625e163af9 --- /dev/null +++ b/tests/components/freebox/test_button.py @@ -0,0 +1,43 @@ +"""Tests for the Freebox config flow.""" +from unittest.mock import Mock, patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_HOST, MOCK_PORT + +from tests.common import MockConfigEntry + + +async def test_reboot_button(hass: HomeAssistant, router: Mock): + """Test reboot button.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, + unique_id=MOCK_HOST, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert hass.config_entries.async_entries() == [entry] + + assert router.call_count == 1 + assert router().open.call_count == 1 + + with patch( + "homeassistant.components.freebox.router.FreeboxRouter.reboot" + ) as mock_service: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + service_data={ + ATTR_ENTITY_ID: "button.reboot_freebox", + }, + blocking=True, + ) + await hass.async_block_till_done() + mock_service.assert_called_once() diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 3bffd213b7276..cee1c28cebda8 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -20,6 +20,7 @@ MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host="192.168.0.254", + addresses=["192.168.0.254"], port=80, hostname="Freebox-Server.local.", type="_fbx-api._tcp.local.", diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 1dc60f4a59ee0..139a8448b08f8 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,115 +1,86 @@ """Common stuff for AVM Fritz!Box tests.""" -from unittest import mock +import logging from unittest.mock import patch +from fritzconnection.core.processor import Service +from fritzconnection.lib.fritzhosts import FritzHosts import pytest +from .const import MOCK_FB_SERVICES, MOCK_MESH_DATA, MOCK_MODELNAME -@pytest.fixture() -def fc_class_mock(): - """Fixture that sets up a mocked FritzConnection class.""" - with patch("fritzconnection.FritzConnection", autospec=True) as result: - result.return_value = FritzConnectionMock() - yield result +LOGGER = logging.getLogger(__name__) + + +class FritzServiceMock(Service): + """Service mocking.""" + + def __init__(self, serviceId: str, actions: dict) -> None: + """Init Service mock.""" + super().__init__() + self._actions = actions + self.serviceId = serviceId class FritzConnectionMock: # pylint: disable=too-few-public-methods """FritzConnection mocking.""" - FRITZBOX_DATA = { - ("WANIPConn:1", "GetStatusInfo"): { - "NewConnectionStatus": "Connected", - "NewUptime": 35307, - }, - ("WANIPConnection:1", "GetStatusInfo"): {}, - ("WANCommonIFC:1", "GetCommonLinkProperties"): { - "NewLayer1DownstreamMaxBitRate": 10087000, - "NewLayer1UpstreamMaxBitRate": 2105000, - "NewPhysicalLinkStatus": "Up", - }, - ("WANCommonIFC:1", "GetAddonInfos"): { - "NewByteSendRate": 3438, - "NewByteReceiveRate": 67649, - "NewTotalBytesSent": 1712232562, - "NewTotalBytesReceived": 5221019883, - }, - ("LANEthernetInterfaceConfig:1", "GetStatistics"): { - "NewBytesSent": 23004321, - "NewBytesReceived": 12045, - }, - ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": "abcdefgh", - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - } - - FRITZBOX_DATA_INDEXED = { - ("X_AVM-DE_Homeauto:1", "GetGenericDeviceInfos"): [ - { - "NewSwitchIsValid": "VALID", - "NewMultimeterIsValid": "VALID", - "NewTemperatureIsValid": "VALID", - "NewDeviceId": 16, - "NewAIN": "08761 0114116", - "NewDeviceName": "FRITZ!DECT 200 #1", - "NewTemperatureOffset": "0", - "NewSwitchLock": "0", - "NewProductName": "FRITZ!DECT 200", - "NewPresent": "CONNECTED", - "NewMultimeterPower": 1673, - "NewHkrComfortTemperature": "0", - "NewSwitchMode": "AUTO", - "NewManufacturer": "AVM", - "NewMultimeterIsEnabled": "ENABLED", - "NewHkrIsTemperature": "0", - "NewFunctionBitMask": 2944, - "NewTemperatureIsEnabled": "ENABLED", - "NewSwitchState": "ON", - "NewSwitchIsEnabled": "ENABLED", - "NewFirmwareVersion": "03.87", - "NewHkrSetVentilStatus": "CLOSED", - "NewMultimeterEnergy": 5182, - "NewHkrComfortVentilStatus": "CLOSED", - "NewHkrReduceTemperature": "0", - "NewHkrReduceVentilStatus": "CLOSED", - "NewHkrIsEnabled": "DISABLED", - "NewHkrSetTemperature": "0", - "NewTemperatureCelsius": "225", - "NewHkrIsValid": "INVALID", - }, - {}, - ], - ("Hosts1", "GetGenericHostEntry"): [ - { - "NewSerialNumber": 1234, - "NewName": "TheName", - "NewModelName": "FRITZ!Box 7490", - }, - {}, - ], - } - - MODELNAME = "FRITZ!Box 7490" - - def __init__(self): + def __init__(self, services): """Inint Mocking class.""" - self.modelname = self.MODELNAME - self.call_action = mock.Mock(side_effect=self._side_effect_call_action) - type(self).action_names = mock.PropertyMock( - side_effect=self._side_effect_action_names - ) + self.modelname = MOCK_MODELNAME + self.call_action = self._call_action + self._services = services self.services = { - srv: None - for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) + srv: FritzServiceMock(serviceId=srv, actions=actions) + for srv, actions in services.items() } + LOGGER.debug("-" * 80) + LOGGER.debug("FritzConnectionMock - services: %s", self.services) + + def _call_action(self, service: str, action: str, **kwargs): + LOGGER.debug( + "_call_action service: %s, action: %s, **kwargs: %s", + service, + action, + {**kwargs}, + ) + if ":" in service: + service, number = service.split(":", 1) + service = service + number + elif not service[-1].isnumeric(): + service = service + "1" - def _side_effect_call_action(self, service, action, **kwargs): if kwargs: - index = next(iter(kwargs.values())) - return self.FRITZBOX_DATA_INDEXED[(service, action)][index] - return self.FRITZBOX_DATA[(service, action)] + if (index := kwargs.get("NewIndex")) is None: + index = next(iter(kwargs.values())) + + return self._services[service][action][index] + return self._services[service][action] + - def _side_effect_action_names(self): - return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) +class FritzHostMock(FritzHosts): + """FritzHosts mocking.""" + + def get_mesh_topology(self, raw=False): + """Retrurn mocked mesh data.""" + return MOCK_MESH_DATA + + +@pytest.fixture() +def fc_class_mock(): + """Fixture that sets up a mocked FritzConnection class.""" + with patch( + "homeassistant.components.fritz.common.FritzConnection", autospec=True + ) as result: + result.return_value = FritzConnectionMock(MOCK_FB_SERVICES) + yield result + + +@pytest.fixture() +def fh_class_mock(): + """Fixture that sets up a mocked FritzHosts class.""" + with patch( + "homeassistant.components.fritz.common.FritzHosts", + new=FritzHostMock, + ) as result: + yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 3212794fc85ec..79d92a1e22de2 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -26,9 +26,758 @@ } } MOCK_HOST = "fake_host" -MOCK_IP = "192.168.178.1" +MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} +MOCK_MODELNAME = "FRITZ!Box 7530 AX" +MOCK_FIRMWARE = "256.07.29" MOCK_SERIAL_NUMBER = "fake_serial_number" MOCK_FIRMWARE_INFO = [True, "1.1.1"] +MOCK_MESH_SSID = "TestSSID" +MOCK_MESH_MASTER_MAC = "1C:ED:6F:12:34:11" +MOCK_MESH_MASTER_WIFI1_MAC = "1C:ED:6F:12:34:12" +MOCK_MESH_SLAVE_MAC = "1C:ED:6F:12:34:21" +MOCK_MESH_SLAVE_WIFI1_MAC = "1C:ED:6F:12:34:22" + +MOCK_FB_SERVICES: dict[str, dict] = { + "DeviceInfo1": { + "GetInfo": { + "NewSerialNumber": MOCK_MESH_MASTER_MAC, + "NewName": "TheName", + "NewModelName": MOCK_MODELNAME, + "NewSoftwareVersion": MOCK_FIRMWARE, + "NewUpTime": 2518179, + }, + }, + "Hosts1": { + "GetGenericHostEntry": [ + { + "NewIPAddress": MOCK_IPS["fritz.box"], + "NewAddressSource": "Static", + "NewLeaseTimeRemaining": 0, + "NewMACAddress": MOCK_MESH_MASTER_MAC, + "NewInterfaceType": "", + "NewActive": True, + "NewHostName": "fritz.box", + }, + { + "NewIPAddress": MOCK_IPS["printer"], + "NewAddressSource": "DHCP", + "NewLeaseTimeRemaining": 0, + "NewMACAddress": "AA:BB:CC:00:11:22", + "NewInterfaceType": "Ethernet", + "NewActive": True, + "NewHostName": "printer", + }, + ], + "X_AVM-DE_GetMeshListPath": {}, + }, + "LANEthernetInterfaceConfig1": { + "GetStatistics": { + "NewBytesSent": 23004321, + "NewBytesReceived": 12045, + }, + }, + "Layer3Forwarding1": { + "GetDefaultConnectionService": { + "NewDefaultConnectionService": "1.WANPPPConnection.1" + } + }, + "UserInterface1": { + "GetInfo": {}, + }, + "WANCommonIFC1": { + "GetCommonLinkProperties": { + "NewLayer1DownstreamMaxBitRate": 10087000, + "NewLayer1UpstreamMaxBitRate": 2105000, + "NewPhysicalLinkStatus": "Up", + }, + "GetAddonInfos": { + "NewByteSendRate": 3438, + "NewByteReceiveRate": 67649, + "NewTotalBytesSent": 1712232562, + "NewTotalBytesReceived": 5221019883, + "NewX_AVM_DE_TotalBytesSent64": 1712232562, + "NeWX_AVM_DE_TotalBytesReceived64": 5221019883, + }, + "GetTotalBytesSent": {"NewTotalBytesSent": 1712232562}, + "GetTotalBytesReceived": {"NewTotalBytesReceived": 5221019883}, + }, + "WANCommonInterfaceConfig1": { + "GetCommonLinkProperties": { + "NewWANAccessType": "DSL", + "NewLayer1UpstreamMaxBitRate": 51805000, + "NewLayer1DownstreamMaxBitRate": 318557000, + "NewPhysicalLinkStatus": "Up", + } + }, + "WANDSLInterfaceConfig1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewDataPath": "Interleaved", + "NewUpstreamCurrRate": 46720, + "NewDownstreamCurrRate": 292030, + "NewUpstreamMaxRate": 51348, + "NewDownstreamMaxRate": 315978, + "NewUpstreamNoiseMargin": 90, + "NewDownstreamNoiseMargin": 80, + "NewUpstreamAttenuation": 70, + "NewDownstreamAttenuation": 120, + "NewATURVendor": "41564d00", + "NewATURCountry": "0400", + "NewUpstreamPower": 500, + "NewDownstreamPower": 500, + } + }, + "WANIPConn1": { + "GetStatusInfo": { + "NewConnectionStatus": "Connected", + "NewUptime": 35307, + }, + "GetExternalIPAddress": {"NewExternalIPAddress": "1.2.3.4"}, + }, + "WANPPPConnection1": { + "GetInfo": { + "NewEnable": True, + "NewConnectionStatus": "Connected", + "NewUptime": 57199, + "NewUpstreamMaxBitRate": 46531924, + "NewDownstreamMaxBitRate": 43430530, + "NewExternalIPAddress": "1.2.3.4", + }, + "GetPortMappingNumberOfEntries": {}, + }, + "X_AVM-DE_Homeauto1": { + "GetGenericDeviceInfos": [ + { + "NewSwitchIsValid": "VALID", + "NewMultimeterIsValid": "VALID", + "NewTemperatureIsValid": "VALID", + "NewDeviceId": 16, + "NewAIN": "08761 0114116", + "NewDeviceName": "FRITZ!DECT 200 #1", + "NewTemperatureOffset": "0", + "NewSwitchLock": "0", + "NewProductName": "FRITZ!DECT 200", + "NewPresent": "CONNECTED", + "NewMultimeterPower": 1673, + "NewHkrComfortTemperature": "0", + "NewSwitchMode": "AUTO", + "NewManufacturer": "AVM", + "NewMultimeterIsEnabled": "ENABLED", + "NewHkrIsTemperature": "0", + "NewFunctionBitMask": 2944, + "NewTemperatureIsEnabled": "ENABLED", + "NewSwitchState": "ON", + "NewSwitchIsEnabled": "ENABLED", + "NewFirmwareVersion": "03.87", + "NewHkrSetVentilStatus": "CLOSED", + "NewMultimeterEnergy": 5182, + "NewHkrComfortVentilStatus": "CLOSED", + "NewHkrReduceTemperature": "0", + "NewHkrReduceVentilStatus": "CLOSED", + "NewHkrIsEnabled": "DISABLED", + "NewHkrSetTemperature": "0", + "NewTemperatureCelsius": "225", + "NewHkrIsValid": "INVALID", + }, + {}, + ], + }, + "X_AVM-DE_HostFilter1": { + "GetWANAccessByIP": { + MOCK_IPS["printer"]: {"NewDisallow": False, "NewWANAccess": "granted"} + } + }, +} + +MOCK_MESH_DATA = { + "schema_version": "1.9", + "nodes": [ + { + "uid": "n-1", + "device_name": "fritz.box", + "device_model": "FRITZ!Box 7530 AX", + "device_manufacturer": "AVM", + "device_firmware_version": "256.07.29", + "device_mac_address": MOCK_MESH_MASTER_MAC, + "is_meshed": True, + "mesh_role": "master", + "meshd_version": "3.13", + "node_interfaces": [ + { + "uid": "ni-5", + "name": "LANBridge", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-30", + "name": "LAN:2", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-32", + "name": "LAN:3", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-31", + "name": "LAN:1", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [ + { + "uid": "nl-78", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-76", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-77", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + }, + { + "uid": "ni-33", + "name": "LAN:4", + "type": "LAN", + "mac_address": MOCK_MESH_MASTER_MAC, + "blocking_state": "NOT_BLOCKED", + "node_links": [], + }, + { + "uid": "ni-230", + "name": "AP:2G:0", + "type": "WLAN", + "mac_address": MOCK_MESH_MASTER_WIFI1_MAC, + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-219", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-89", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-90", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 51, + "tx_rsni": 255, + "rx_rcpi": -38, + "tx_rcpi": 255, + }, + { + "uid": "nl-168", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645162418, + "node_1_uid": "n-1", + "node_2_uid": "n-118", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-119", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 37, + "tx_rsni": 255, + "rx_rcpi": -52, + "tx_rcpi": 255, + }, + { + "uid": "nl-185", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645273363, + "node_1_uid": "n-1", + "node_2_uid": "n-100", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-99", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 1000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 35, + "tx_rsni": 255, + "rx_rcpi": -54, + "tx_rcpi": 255, + }, + { + "uid": "nl-166", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618912, + "node_1_uid": "n-1", + "node_2_uid": "n-16", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-15", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 41, + "tx_rsni": 255, + "rx_rcpi": -48, + "tx_rcpi": 255, + }, + { + "uid": "nl-239", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618828, + "node_1_uid": "n-1", + "node_2_uid": "n-59", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-58", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 43, + "tx_rsni": 255, + "rx_rcpi": -46, + "tx_rcpi": 255, + }, + { + "uid": "nl-173", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1645331764, + "node_1_uid": "n-1", + "node_2_uid": "n-137", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-138", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 72200, + "cur_data_rate_tx": 65000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 38, + "tx_rsni": 255, + "rx_rcpi": -51, + "tx_rcpi": 255, + }, + { + "uid": "nl-217", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618833, + "node_1_uid": "n-1", + "node_2_uid": "n-128", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-127", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 72200, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 41, + "tx_rsni": 255, + "rx_rcpi": -48, + "tx_rcpi": 255, + }, + { + "uid": "nl-198", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-105", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-106", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 58500, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 28, + "tx_rsni": 255, + "rx_rcpi": -61, + "tx_rcpi": 255, + }, + { + "uid": "nl-213", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-111", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-112", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 44, + "tx_rsni": 255, + "rx_rcpi": -45, + "tx_rcpi": 255, + }, + { + "uid": "nl-224", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618831, + "node_1_uid": "n-1", + "node_2_uid": "n-197", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-196", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 51, + "tx_rsni": 255, + "rx_rcpi": -38, + "tx_rcpi": 255, + }, + { + "uid": "nl-182", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618822, + "node_1_uid": "n-1", + "node_2_uid": "n-56", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-55", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 72200, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 34, + "tx_rsni": 255, + "rx_rcpi": -55, + "tx_rcpi": 255, + }, + { + "uid": "nl-205", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618820, + "node_1_uid": "n-1", + "node_2_uid": "n-109", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-108", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 54000, + "cur_data_rate_tx": 1000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 43, + "tx_rsni": 255, + "rx_rcpi": -46, + "tx_rcpi": 255, + }, + { + "uid": "nl-240", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618827, + "node_1_uid": "n-1", + "node_2_uid": "n-95", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-96", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 58500, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 25, + "tx_rsni": 255, + "rx_rcpi": -64, + "tx_rcpi": 255, + }, + { + "uid": "nl-146", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-167", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-134", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 48, + "tx_rsni": 255, + "rx_rcpi": -41, + "tx_rcpi": 255, + }, + { + "uid": "nl-232", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1644618829, + "node_1_uid": "n-1", + "node_2_uid": "n-18", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-17", + "max_data_rate_rx": 72200, + "max_data_rate_tx": 72200, + "cur_data_rate_rx": 48000, + "cur_data_rate_tx": 21700, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 22, + "tx_rsni": 255, + "rx_rcpi": -67, + "tx_rcpi": 255, + }, + ], + "ssid": MOCK_MESH_SSID, + "opmode": "AP", + "security": "WPA2_WPA3_MIXED", + "supported_streams_tx": [ + ["20 MHz", 2], + ["40 MHz", 0], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "supported_streams_rx": [ + ["20 MHz", 2], + ["40 MHz", 0], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "current_channel": 13, + "phymodes": ["g", "n", "ax"], + "channel_utilization": 0, + "anpi": -91, + "steering_enabled": True, + "11k_friendly": True, + "11v_friendly": True, + "legacy_friendly": True, + "rrm_compliant": False, + "channel_list": [ + {"channel": 1}, + {"channel": 2}, + {"channel": 3}, + {"channel": 4}, + {"channel": 5}, + {"channel": 6}, + {"channel": 7}, + {"channel": 8}, + {"channel": 9}, + {"channel": 10}, + {"channel": 11}, + {"channel": 12}, + {"channel": 13}, + ], + }, + ], + }, + { + "uid": "n-76", + "device_name": "printer", + "device_model": "", + "device_manufacturer": "", + "device_firmware_version": "", + "device_mac_address": "AA:BB:CC:00:11:22", + "is_meshed": False, + "mesh_role": "unknown", + "meshd_version": "0.0", + "node_interfaces": [ + { + "uid": "ni-77", + "name": "eth0", + "type": "LAN", + "mac_address": "AA:BB:CC:00:11:22", + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-78", + "type": "LAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-76", + "node_interface_1_uid": "ni-31", + "node_interface_2_uid": "ni-77", + "max_data_rate_rx": 1000000, + "max_data_rate_tx": 1000000, + "cur_data_rate_rx": 0, + "cur_data_rate_tx": 0, + "cur_availability_rx": 99, + "cur_availability_tx": 99, + } + ], + } + ], + }, + { + "uid": "n-167", + "device_name": "fritz-repeater", + "device_model": "FRITZ!Box 7490", + "device_manufacturer": "AVM", + "device_firmware_version": "113.07.29", + "device_mac_address": MOCK_MESH_SLAVE_MAC, + "is_meshed": True, + "mesh_role": "slave", + "meshd_version": "3.13", + "node_interfaces": [ + { + "uid": "ni-140", + "name": "LAN:3", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-139", + "name": "LAN:4", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-141", + "name": "LAN:2", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-134", + "name": "UPLINK:2G:0", + "type": "WLAN", + "mac_address": MOCK_MESH_SLAVE_WIFI1_MAC, + "blocking_state": "UNKNOWN", + "node_links": [ + { + "uid": "nl-146", + "type": "WLAN", + "state": "CONNECTED", + "last_connected": 1642872967, + "node_1_uid": "n-1", + "node_2_uid": "n-167", + "node_interface_1_uid": "ni-230", + "node_interface_2_uid": "ni-134", + "max_data_rate_rx": 144400, + "max_data_rate_tx": 144400, + "cur_data_rate_rx": 144400, + "cur_data_rate_tx": 130000, + "cur_availability_rx": 100, + "cur_availability_tx": 100, + "rx_rsni": 48, + "tx_rsni": 255, + "rx_rcpi": -41, + "tx_rcpi": 255, + } + ], + "ssid": "", + "opmode": "WDS_REPEATER", + "security": "WPA3PSK", + "supported_streams_tx": [ + ["20 MHz", 3], + ["40 MHz", 3], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "supported_streams_rx": [ + ["20 MHz", 3], + ["40 MHz", 3], + ["80 MHz", 0], + ["160 MHz", 0], + ["80+80 MHz", 0], + ], + "current_channel": 13, + "phymodes": ["b", "g", "n"], + "channel_utilization": 0, + "anpi": 255, + "steering_enabled": True, + "11k_friendly": False, + "11v_friendly": True, + "legacy_friendly": True, + "rrm_compliant": False, + "channel_list": [ + {"channel": 1}, + {"channel": 2}, + {"channel": 3}, + {"channel": 4}, + {"channel": 5}, + {"channel": 6}, + {"channel": 7}, + {"channel": 8}, + {"channel": 9}, + {"channel": 10}, + {"channel": 11}, + {"channel": 12}, + {"channel": 13}, + ], + "client_position": "unknown", + }, + { + "uid": "ni-143", + "name": "LANBridge", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + { + "uid": "ni-142", + "name": "LAN:1", + "type": "LAN", + "mac_address": MOCK_MESH_SLAVE_MAC, + "blocking_state": "UNKNOWN", + "node_links": [], + }, + ], + }, + ], +} + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_DEVICE_INFO = { @@ -38,7 +787,7 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location=f"https://{MOCK_IP}:12345/test", + ssdp_location=f"https://{MOCK_IPS['fritz.box']}:12345/test", upnp={ ATTR_UPNP_FRIENDLY_NAME: "fake_name", ATTR_UPNP_UDN: "uuid:only-a-test", diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py new file mode 100644 index 0000000000000..a6ff579958a59 --- /dev/null +++ b/tests/components/fritz/test_button.py @@ -0,0 +1,75 @@ +"""Tests for Shelly button platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_button_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup of Fritz!Tools buttons.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + buttons = hass.states.async_all(BUTTON_DOMAIN) + assert len(buttons) == 4 + + for button in buttons: + assert button.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "entity_id, wrapper_method", + [ + ("button.mock_title_firmware_update", "async_trigger_firmware_update"), + ("button.mock_title_reboot", "async_trigger_reboot"), + ("button.mock_title_reconnect", "async_trigger_reconnect"), + ("button.mock_title_cleanup", "async_trigger_cleanup"), + ], +) +async def test_buttons( + hass: HomeAssistant, + entity_id: str, + wrapper_method: str, + fc_class_mock, + fh_class_mock, +): + """Test Fritz!Tools buttons.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + button = hass.states.get(entity_id) + assert button + assert button.state == STATE_UNKNOWN + with patch( + f"homeassistant.components.fritz.common.AvmWrapper.{wrapper_method}" + ) as mock_press_action: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + mock_press_action.assert_called_once() + + button = hass.states.get(entity_id) + assert button.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6505ee2bcaaf7..502757c2c67e2 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -26,7 +26,7 @@ from .const import ( MOCK_FIRMWARE_INFO, - MOCK_IP, + MOCK_IPS, MOCK_REQUEST, MOCK_SSDP_DATA, MOCK_USER_DATA, @@ -51,7 +51,7 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.post" ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): mock_request_get.return_value.status_code = 200 @@ -102,7 +102,7 @@ async def test_user_already_configured( "requests.post" ) as mock_request_post, patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): mock_request_get.return_value.status_code = 200 @@ -295,7 +295,7 @@ async def test_ssdp_already_configured( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -322,7 +322,7 @@ async def test_ssdp_already_configured_host( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -349,7 +349,7 @@ async def test_ssdp_already_configured_host_uuid( side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IP, + return_value=MOCK_IPS["fritz.box"], ): result = await hass.config_entries.flow.async_init( @@ -420,7 +420,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_HOST] == MOCK_IP + assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py new file mode 100644 index 0000000000000..892210d08442b --- /dev/null +++ b/tests/components/fritz/test_diagnostics.py @@ -0,0 +1,80 @@ +"""Tests for the AVM Fritz!Box integration.""" +from __future__ import annotations + +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.fritz.common import AvmWrapper +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.components.fritz.diagnostics import TO_REDACT +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, fc_class_mock, fh_class_mock +): + """Test config entry diagnostics.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + entry_dict = entry.as_dict() + for key in TO_REDACT: + entry_dict["data"][key] = REDACTED + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + assert result == { + "entry": entry_dict, + "device_info": { + "client_devices": [ + { + "connected_to": device.connected_to, + "connection_type": device.connection_type, + "hostname": device.hostname, + "is_connected": device.is_connected, + "last_activity": device.last_activity.isoformat(), + "wan_access": device.wan_access, + } + for _, device in avm_wrapper.devices.items() + ], + "connection_type": "WANPPPConnection", + "current_firmware": "256.07.29", + "discovered_services": [ + "DeviceInfo1", + "Hosts1", + "LANEthernetInterfaceConfig1", + "Layer3Forwarding1", + "UserInterface1", + "WANCommonIFC1", + "WANCommonInterfaceConfig1", + "WANDSLInterfaceConfig1", + "WANIPConn1", + "WANPPPConnection1", + "X_AVM-DE_Homeauto1", + "X_AVM-DE_HostFilter1", + ], + "is_router": True, + "last_exception": None, + "last_update success": True, + "latest_firmware": None, + "mesh_role": "master", + "model": "FRITZ!Box 7530 AX", + "update_available": False, + "wan_link_properties": { + "NewLayer1DownstreamMaxBitRate": 318557000, + "NewLayer1UpstreamMaxBitRate": 51805000, + "NewPhysicalLinkStatus": "Up", + "NewWANAccessType": "DSL", + }, + }, + } diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py new file mode 100644 index 0000000000000..fd67321d235cb --- /dev/null +++ b/tests/components/fritz/test_init.py @@ -0,0 +1,96 @@ +"""Tests for AVM Fritz!Box.""" +from unittest.mock import patch + +from fritzconnection.core.exceptions import FritzSecurityError +import pytest + +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) +from homeassistant.components.fritz.const import DOMAIN, FRITZ_EXCEPTIONS +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test setup and unload of Fritz!Tools.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED + + +async def test_options_reload(hass: HomeAssistant, fc_class_mock, fh_class_mock): + """Test reload of Fritz!Tools, when options changed.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_USER_DATA, + options={CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds()}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=None, + ) as mock_reload: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_CONSIDER_HOME: 60}, + ) + await hass.async_block_till_done() + mock_reload.assert_called_once() + + +async def test_setup_auth_fail(hass: HomeAssistant): + """Test starting a flow by user with an already configured device.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=FritzSecurityError, + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.SETUP_ERROR + + +@pytest.mark.parametrize( + "error", + FRITZ_EXCEPTIONS, +) +async def test_setup_fail(hass: HomeAssistant, error): + """Test starting a flow by user with an already configured device.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=error, + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/github/common.py b/tests/components/github/common.py index a99834f0cbdf1..a75a8cfaa7850 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -31,12 +31,11 @@ async def setup_github_integration( }, headers=headers, ) - for endpoint in ("issues", "pulls", "releases", "commits"): - aioclient_mock.get( - f"https://api.github.com/repos/{repository}/{endpoint}", - json=json.loads(load_fixture(f"{endpoint}.json", DOMAIN)), - headers=headers, - ) + aioclient_mock.post( + "https://api.github.com/graphql", + json=json.loads(load_fixture("graphql.json", DOMAIN)), + headers=headers, + ) mock_config_entry.add_to_hass(hass) setup_result = await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/github/fixtures/commits.json b/tests/components/github/fixtures/commits.json deleted file mode 100644 index c0deeaf51eaf2..0000000000000 --- a/tests/components/github/fixtures/commits.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", - "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==", - "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments", - "commit": { - "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "author": { - "name": "Monalisa Octocat", - "email": "support@github.com", - "date": "2011-04-14T16:00:49Z" - }, - "committer": { - "name": "Monalisa Octocat", - "email": "support@github.com", - "date": "2011-04-14T16:00:49Z" - }, - "message": "Fix all the bugs", - "tree": { - "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" - }, - "comment_count": 0, - "verification": { - "verified": false, - "reason": "unsigned", - "signature": null, - "payload": null - } - }, - "author": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "committer": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "parents": [ - { - "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" - } - ] - } -] \ No newline at end of file diff --git a/tests/components/github/fixtures/graphql.json b/tests/components/github/fixtures/graphql.json new file mode 100644 index 0000000000000..52b0e6ccfd60e --- /dev/null +++ b/tests/components/github/fixtures/graphql.json @@ -0,0 +1,69 @@ +{ + "data": { + "rateLimit": { + "cost": 1, + "remaining": 4999 + }, + "repository": { + "default_branch_ref": { + "commit": { + "message": "Fix all the bugs", + "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + }, + "stargazers_count": 9, + "forks_count": 9, + "full_name": "octocat/Hello-World", + "id": 1296269, + "watchers": { + "total": 9 + }, + "discussion": { + "total": 1, + "discussions": [ + { + "title": "First discussion", + "url": "https://github.com/octocat/Hello-World/discussions/1347", + "number": 1347 + } + ] + }, + "issue": { + "total": 1, + "issues": [ + { + "title": "Found a bug", + "url": "https://github.com/octocat/Hello-World/issues/1347", + "number": 1347 + } + ] + }, + "pull_request": { + "total": 1, + "pull_requests": [ + { + "title": "Amazing new feature", + "url": "https://github.com/octocat/Hello-World/pull/1347", + "number": 1347 + } + ] + }, + "release": { + "name": "v1.0.0", + "url": "https://github.com/octocat/Hello-World/releases/v1.0.0", + "tag": "v1.0.0" + }, + "refs": { + "tags": [ + { + "name": "v1.0.0", + "target": { + "url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/components/github/fixtures/issues.json b/tests/components/github/fixtures/issues.json deleted file mode 100644 index d59f1f5c79675..0000000000000 --- a/tests/components/github/fixtures/issues.json +++ /dev/null @@ -1,159 +0,0 @@ -[ - { - "id": 1, - "node_id": "MDU6SXNzdWUx", - "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", - "repository_url": "https://api.github.com/repos/octocat/Hello-World", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", - "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events", - "html_url": "https://github.com/octocat/Hello-World/issues/1347", - "number": 1347, - "state": "open", - "title": "Found a bug", - "body": "I'm having a problem with this.", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "labels": [ - { - "id": 208045946, - "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", - "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", - "name": "bug", - "description": "Something isn't working", - "color": "f29513", - "default": true - } - ], - "assignee": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "assignees": [ - { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - } - ], - "milestone": { - "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", - "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", - "id": 1002604, - "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", - "number": 1, - "state": "open", - "title": "v1.0", - "description": "Tracking milestone for version 1.0", - "creator": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "open_issues": 4, - "closed_issues": 8, - "created_at": "2011-04-10T20:09:31Z", - "updated_at": "2014-03-03T18:58:10Z", - "closed_at": "2013-02-12T13:22:01Z", - "due_on": "2012-10-09T23:39:01Z" - }, - "locked": true, - "active_lock_reason": "too heated", - "comments": 0, - "pull_request": { - "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", - "html_url": "https://github.com/octocat/Hello-World/pull/1347", - "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", - "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch" - }, - "closed_at": null, - "created_at": "2011-04-22T13:33:48Z", - "updated_at": "2011-04-22T13:33:48Z", - "closed_by": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "author_association": "COLLABORATOR" - } -] \ No newline at end of file diff --git a/tests/components/github/fixtures/pulls.json b/tests/components/github/fixtures/pulls.json deleted file mode 100644 index a42763b18d8a9..0000000000000 --- a/tests/components/github/fixtures/pulls.json +++ /dev/null @@ -1,520 +0,0 @@ -[ - { - "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", - "id": 1, - "node_id": "MDExOlB1bGxSZXF1ZXN0MQ==", - "html_url": "https://github.com/octocat/Hello-World/pull/1347", - "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", - "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch", - "issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", - "commits_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits", - "review_comments_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments", - "review_comment_url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e", - "number": 1347, - "state": "open", - "locked": true, - "title": "Amazing new feature", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "body": "Please pull these awesome changes in!", - "labels": [ - { - "id": 208045946, - "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", - "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", - "name": "bug", - "description": "Something isn't working", - "color": "f29513", - "default": true - } - ], - "milestone": { - "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", - "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", - "id": 1002604, - "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", - "number": 1, - "state": "open", - "title": "v1.0", - "description": "Tracking milestone for version 1.0", - "creator": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "open_issues": 4, - "closed_issues": 8, - "created_at": "2011-04-10T20:09:31Z", - "updated_at": "2014-03-03T18:58:10Z", - "closed_at": "2013-02-12T13:22:01Z", - "due_on": "2012-10-09T23:39:01Z" - }, - "active_lock_reason": "too heated", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:01:12Z", - "closed_at": "2011-01-26T19:01:12Z", - "merged_at": "2011-01-26T19:01:12Z", - "merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6", - "assignee": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "assignees": [ - { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - { - "login": "hubot", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/hubot_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/hubot", - "html_url": "https://github.com/hubot", - "followers_url": "https://api.github.com/users/hubot/followers", - "following_url": "https://api.github.com/users/hubot/following{/other_user}", - "gists_url": "https://api.github.com/users/hubot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/hubot/subscriptions", - "organizations_url": "https://api.github.com/users/hubot/orgs", - "repos_url": "https://api.github.com/users/hubot/repos", - "events_url": "https://api.github.com/users/hubot/events{/privacy}", - "received_events_url": "https://api.github.com/users/hubot/received_events", - "type": "User", - "site_admin": true - } - ], - "requested_reviewers": [ - { - "login": "other_user", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/other_user_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/other_user", - "html_url": "https://github.com/other_user", - "followers_url": "https://api.github.com/users/other_user/followers", - "following_url": "https://api.github.com/users/other_user/following{/other_user}", - "gists_url": "https://api.github.com/users/other_user/gists{/gist_id}", - "starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/other_user/subscriptions", - "organizations_url": "https://api.github.com/users/other_user/orgs", - "repos_url": "https://api.github.com/users/other_user/repos", - "events_url": "https://api.github.com/users/other_user/events{/privacy}", - "received_events_url": "https://api.github.com/users/other_user/received_events", - "type": "User", - "site_admin": false - } - ], - "requested_teams": [ - { - "id": 1, - "node_id": "MDQ6VGVhbTE=", - "url": "https://api.github.com/teams/1", - "html_url": "https://github.com/orgs/github/teams/justice-league", - "name": "Justice League", - "slug": "justice-league", - "description": "A great team.", - "privacy": "closed", - "permission": "admin", - "members_url": "https://api.github.com/teams/1/members{/member}", - "repositories_url": "https://api.github.com/teams/1/repos", - "parent": null - } - ], - "head": { - "label": "octocat:new-topic", - "ref": "new-topic", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "repo": { - "id": 1296269, - "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", - "name": "Hello-World", - "full_name": "octocat/Hello-World", - "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "private": false, - "html_url": "https://github.com/octocat/Hello-World", - "description": "This your first repo!", - "fork": false, - "url": "https://api.github.com/repos/octocat/Hello-World", - "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", - "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", - "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", - "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", - "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", - "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", - "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", - "events_url": "https://api.github.com/repos/octocat/Hello-World/events", - "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", - "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", - "git_url": "git:github.com/octocat/Hello-World.git", - "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", - "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", - "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", - "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", - "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", - "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", - "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", - "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", - "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", - "ssh_url": "git@github.com:octocat/Hello-World.git", - "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", - "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", - "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", - "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", - "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", - "clone_url": "https://github.com/octocat/Hello-World.git", - "mirror_url": "git:git.example.com/octocat/Hello-World", - "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", - "svn_url": "https://svn.github.com/octocat/Hello-World", - "homepage": "https://github.com", - "language": null, - "forks_count": 9, - "stargazers_count": 80, - "watchers_count": 80, - "size": 108, - "default_branch": "master", - "open_issues_count": 0, - "is_template": true, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], - "has_issues": true, - "has_projects": true, - "has_wiki": true, - "has_pages": false, - "has_downloads": true, - "archived": false, - "disabled": false, - "visibility": "public", - "pushed_at": "2011-01-26T19:06:43Z", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:14:43Z", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "allow_rebase_merge": true, - "template_repository": null, - "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", - "allow_squash_merge": true, - "allow_auto_merge": false, - "delete_branch_on_merge": true, - "allow_merge_commit": true, - "subscribers_count": 42, - "network_count": 0, - "license": { - "key": "mit", - "name": "MIT License", - "url": "https://api.github.com/licenses/mit", - "spdx_id": "MIT", - "node_id": "MDc6TGljZW5zZW1pdA==", - "html_url": "https://github.com/licenses/mit" - }, - "forks": 1, - "open_issues": 1, - "watchers": 1 - } - }, - "base": { - "label": "octocat:master", - "ref": "master", - "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", - "user": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "repo": { - "id": 1296269, - "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", - "name": "Hello-World", - "full_name": "octocat/Hello-World", - "owner": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "private": false, - "html_url": "https://github.com/octocat/Hello-World", - "description": "This your first repo!", - "fork": false, - "url": "https://api.github.com/repos/octocat/Hello-World", - "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", - "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", - "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", - "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", - "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", - "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", - "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", - "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", - "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", - "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", - "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", - "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", - "events_url": "https://api.github.com/repos/octocat/Hello-World/events", - "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", - "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", - "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", - "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", - "git_url": "git:github.com/octocat/Hello-World.git", - "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", - "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", - "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", - "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", - "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", - "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", - "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", - "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", - "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", - "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", - "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", - "ssh_url": "git@github.com:octocat/Hello-World.git", - "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", - "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", - "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", - "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", - "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", - "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", - "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", - "clone_url": "https://github.com/octocat/Hello-World.git", - "mirror_url": "git:git.example.com/octocat/Hello-World", - "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", - "svn_url": "https://svn.github.com/octocat/Hello-World", - "homepage": "https://github.com", - "language": null, - "forks_count": 9, - "stargazers_count": 80, - "watchers_count": 80, - "size": 108, - "default_branch": "master", - "open_issues_count": 0, - "is_template": true, - "topics": [ - "octocat", - "atom", - "electron", - "api" - ], - "has_issues": true, - "has_projects": true, - "has_wiki": true, - "has_pages": false, - "has_downloads": true, - "archived": false, - "disabled": false, - "visibility": "public", - "pushed_at": "2011-01-26T19:06:43Z", - "created_at": "2011-01-26T19:01:12Z", - "updated_at": "2011-01-26T19:14:43Z", - "permissions": { - "admin": false, - "push": false, - "pull": true - }, - "allow_rebase_merge": true, - "template_repository": null, - "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", - "allow_squash_merge": true, - "allow_auto_merge": false, - "delete_branch_on_merge": true, - "allow_merge_commit": true, - "subscribers_count": 42, - "network_count": 0, - "license": { - "key": "mit", - "name": "MIT License", - "url": "https://api.github.com/licenses/mit", - "spdx_id": "MIT", - "node_id": "MDc6TGljZW5zZW1pdA==", - "html_url": "https://github.com/licenses/mit" - }, - "forks": 1, - "open_issues": 1, - "watchers": 1 - } - }, - "_links": { - "self": { - "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347" - }, - "html": { - "href": "https://github.com/octocat/Hello-World/pull/1347" - }, - "issue": { - "href": "https://api.github.com/repos/octocat/Hello-World/issues/1347" - }, - "comments": { - "href": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments" - }, - "review_comments": { - "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments" - }, - "review_comment": { - "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}" - }, - "commits": { - "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits" - }, - "statuses": { - "href": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e" - } - }, - "author_association": "OWNER", - "auto_merge": null, - "draft": false - } -] \ No newline at end of file diff --git a/tests/components/github/fixtures/releases.json b/tests/components/github/fixtures/releases.json deleted file mode 100644 index e69206ae78401..0000000000000 --- a/tests/components/github/fixtures/releases.json +++ /dev/null @@ -1,76 +0,0 @@ -[ - { - "url": "https://api.github.com/repos/octocat/Hello-World/releases/1", - "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0", - "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets", - "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}", - "tarball_url": "https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0", - "zipball_url": "https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0", - "id": 1, - "node_id": "MDc6UmVsZWFzZTE=", - "tag_name": "v1.0.0", - "target_commitish": "master", - "name": "v1.0.0", - "body": "Description of the release", - "draft": false, - "prerelease": false, - "created_at": "2013-02-27T19:35:32Z", - "published_at": "2013-02-27T19:35:32Z", - "author": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - }, - "assets": [ - { - "url": "https://api.github.com/repos/octocat/Hello-World/releases/assets/1", - "browser_download_url": "https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip", - "id": 1, - "node_id": "MDEyOlJlbGVhc2VBc3NldDE=", - "name": "example.zip", - "label": "short description", - "state": "uploaded", - "content_type": "application/zip", - "size": 1024, - "download_count": 42, - "created_at": "2013-02-27T19:35:32Z", - "updated_at": "2013-02-27T19:35:32Z", - "uploader": { - "login": "octocat", - "id": 1, - "node_id": "MDQ6VXNlcjE=", - "avatar_url": "https://github.com/images/error/octocat_happy.gif", - "gravatar_id": "", - "url": "https://api.github.com/users/octocat", - "html_url": "https://github.com/octocat", - "followers_url": "https://api.github.com/users/octocat/followers", - "following_url": "https://api.github.com/users/octocat/following{/other_user}", - "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", - "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", - "organizations_url": "https://api.github.com/users/octocat/orgs", - "repos_url": "https://api.github.com/users/octocat/repos", - "events_url": "https://api.github.com/users/octocat/events{/privacy}", - "received_events_url": "https://api.github.com/users/octocat/received_events", - "type": "User", - "site_admin": false - } - } - ] - } -] \ No newline at end of file diff --git a/tests/components/github/test_diagnostics.py b/tests/components/github/test_diagnostics.py index 6e5e6e13fa44d..80dfaec244597 100644 --- a/tests/components/github/test_diagnostics.py +++ b/tests/components/github/test_diagnostics.py @@ -1,14 +1,16 @@ """Test GitHub diagnostics.""" +import json + from aiogithubapi import GitHubException from aiohttp import ClientSession -from homeassistant.components.github.const import CONF_REPOSITORIES +from homeassistant.components.github.const import CONF_REPOSITORIES, DOMAIN from homeassistant.core import HomeAssistant from .common import setup_github_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.test_util.aiohttp import AiohttpClientMocker @@ -21,13 +23,21 @@ async def test_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" mock_config_entry.options = {CONF_REPOSITORIES: ["home-assistant/core"]} - await setup_github_integration(hass, mock_config_entry, aioclient_mock) + response_json = json.loads(load_fixture("graphql.json", DOMAIN)) + response_json["data"]["repository"]["full_name"] = "home-assistant/core" + + aioclient_mock.post( + "https://api.github.com/graphql", + json=response_json, + headers=json.loads(load_fixture("base_headers.json", DOMAIN)), + ) aioclient_mock.get( "https://api.github.com/rate_limit", json={"resources": {"core": {"remaining": 100, "limit": 100}}}, headers={"Content-Type": "application/json"}, ) + await setup_github_integration(hass, mock_config_entry, aioclient_mock) result = await get_diagnostics_for_config_entry( hass, hass_client, diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index cea3edc6b47b8..cba787cbc2873 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -1,62 +1,37 @@ """Test GitHub sensor.""" -from unittest.mock import MagicMock, patch +import json -from aiogithubapi import GitHubNotModifiedException -import pytest - -from homeassistant.components.github.const import DEFAULT_UPDATE_INTERVAL +from homeassistant.components.github.const import DEFAULT_UPDATE_INTERVAL, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.util import dt -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker TEST_SENSOR_ENTITY = "sensor.octocat_hello_world_latest_release" -async def test_sensor_updates_with_not_modified_content( - hass: HomeAssistant, - init_integration: MockConfigEntry, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the sensor updates by default GitHub sensors.""" - state = hass.states.get(TEST_SENSOR_ENTITY) - assert state.state == "v1.0.0" - assert ( - "Content for octocat/Hello-World with RepositoryReleaseDataUpdateCoordinator not modified" - not in caplog.text - ) - - with patch( - "aiogithubapi.namespaces.releases.GitHubReleasesNamespace.list", - side_effect=GitHubNotModifiedException, - ): - - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) - await hass.async_block_till_done() - - assert ( - "Content for octocat/Hello-World with RepositoryReleaseDataUpdateCoordinator not modified" - in caplog.text - ) - new_state = hass.states.get(TEST_SENSOR_ENTITY) - assert state.state == new_state.state - - async def test_sensor_updates_with_empty_release_array( hass: HomeAssistant, init_integration: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the sensor updates by default GitHub sensors.""" state = hass.states.get(TEST_SENSOR_ENTITY) assert state.state == "v1.0.0" - with patch( - "aiogithubapi.namespaces.releases.GitHubReleasesNamespace.list", - return_value=MagicMock(data=[]), - ): + response_json = json.loads(load_fixture("graphql.json", DOMAIN)) + response_json["data"]["repository"]["release"] = None + + aioclient_mock.clear_requests() + aioclient_mock.post( + "https://api.github.com/graphql", + json=response_json, + headers=json.loads(load_fixture("base_headers.json", DOMAIN)), + ) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) - await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) + await hass.async_block_till_done() new_state = hass.states.get(TEST_SENSOR_ENTITY) assert new_state.state == "unavailable" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index a20687c5f3d1e..713295c0efdcc 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -109,6 +109,7 @@ async def test_form_homekit_unique_id_already_setup(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -136,6 +137,7 @@ async def test_form_homekit_unique_id_already_setup(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -160,6 +162,7 @@ async def test_form_homekit_ip_address_already_setup(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -178,6 +181,7 @@ async def test_form_homekit_ip_address(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -260,6 +264,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 20cb13130ec63..59de9b2cddf9b 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,10 +1,20 @@ """Test configuration and mocks for the google integration.""" -from unittest.mock import patch +from collections.abc import Callable +from typing import Any, Generator, TypeVar +from unittest.mock import Mock, patch import pytest +from homeassistant.components.google import GoogleCalendarService + +ApiResult = Callable[[dict[str, Any]], None] +T = TypeVar("T") +YieldFixture = Generator[T, None, None] + + +CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" TEST_CALENDAR = { - "id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", + "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", "accessRole": "reader", @@ -34,3 +44,45 @@ def mock_next_event(): ) with patch_google_cal as google_cal_data: yield google_cal_data + + +@pytest.fixture +def mock_events_list( + google_service: GoogleCalendarService, +) -> Callable[[dict[str, Any]], None]: + """Fixture to construct a fake event list API response.""" + + def _put_result(response: dict[str, Any]) -> None: + google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = ( + response + ) + return + + return _put_result + + +@pytest.fixture +def mock_calendars_list( + google_service: GoogleCalendarService, +) -> ApiResult: + """Fixture to construct a fake calendar list API response.""" + + def _put_result(response: dict[str, Any]) -> None: + google_service.return_value.get.return_value.calendarList.return_value.list.return_value.execute.return_value = ( + response + ) + return + + return _put_result + + +@pytest.fixture +def mock_insert_event( + google_service: GoogleCalendarService, +) -> Mock: + """Fixture to create a mock to capture new events added to the API.""" + insert_mock = Mock() + google_service.return_value.get.return_value.events.return_value.insert = ( + insert_mock + ) + return insert_mock diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 01bd179e2ea11..0ee257788dd0c 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Callable import copy from http import HTTPStatus from typing import Any @@ -22,7 +21,6 @@ CONF_TRACK, DEVICE_SCHEMA, SERVICE_SCAN_CALENDARS, - GoogleCalendarService, do_setup, ) from homeassistant.const import STATE_OFF, STATE_ON @@ -358,21 +356,6 @@ async def test_http_event_api_failure(hass, hass_client, google_service): assert events == [] -@pytest.fixture -def mock_events_list( - google_service: GoogleCalendarService, -) -> Callable[[dict[str, Any]], None]: - """Fixture to construct a fake event list API response.""" - - def _put_result(response: dict[str, Any]) -> None: - google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = ( - response - ) - return - - return _put_result - - async def test_http_api_event(hass, hass_client, google_service, mock_events_list): """Test querying the API and fetching events from the server.""" now = dt_util.now() diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index d90efa29f6c8a..c3754511b048e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,67 +1,497 @@ """The tests for the Google Calendar component.""" -from unittest.mock import patch +from collections.abc import Awaitable, Callable +import datetime +from typing import Any +from unittest.mock import Mock, call, mock_open, patch +from oauth2client.client import ( + FlowExchangeError, + OAuth2Credentials, + OAuth2DeviceCodeError, +) import pytest +import yaml -import homeassistant.components.google as google -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.components.google import ( + DOMAIN, + SERVICE_ADD_EVENT, + GoogleCalendarService, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow +from .conftest import CALENDAR_ID, ApiResult, YieldFixture -@pytest.fixture(name="google_setup") -def mock_google_setup(hass): - """Mock the google set up functions.""" - p_auth = patch( - "homeassistant.components.google.do_authentication", side_effect=google.do_setup +from tests.common import async_fire_time_changed + +# Typing helpers +ComponentSetup = Callable[[], Awaitable[bool]] +HassApi = Callable[[], Awaitable[dict[str, Any]]] + +CODE_CHECK_INTERVAL = 1 +CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) + + +@pytest.fixture +async def code_expiration_delta() -> datetime.timedelta: + """Fixture for code expiration time, defaulting to the future.""" + return datetime.timedelta(minutes=3) + + +@pytest.fixture +async def mock_code_flow( + code_expiration_delta: datetime.timedelta, +) -> YieldFixture[Mock]: + """Fixture for initiating OAuth flow.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + ) as mock_flow: + mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta + mock_flow.return_value.interval = CODE_CHECK_INTERVAL + yield mock_flow + + +@pytest.fixture +async def token_scopes() -> list[str]: + """Fixture for scopes used during test.""" + return ["https://www.googleapis.com/auth/calendar"] + + +@pytest.fixture +async def creds(token_scopes: list[str]) -> OAuth2Credentials: + """Fixture that defines creds used in the test.""" + token_expiry = utcnow() + datetime.timedelta(days=7) + return OAuth2Credentials( + access_token="ACCESS_TOKEN", + client_id="client-id", + client_secret="client-secret", + refresh_token="REFRESH_TOKEN", + token_expiry=token_expiry, + token_uri="http://example.com", + user_agent="n/a", + scopes=token_scopes, ) - p_service = patch("homeassistant.components.google.GoogleCalendarService.get") - p_discovery = patch("homeassistant.components.google.discovery.load_platform") - p_load = patch("homeassistant.components.google.load_config", return_value={}) - p_save = patch("homeassistant.components.google.update_config") - with p_auth, p_load, p_service, p_discovery, p_save: + +@pytest.fixture +async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: + """Fixture for mocking out the exchange for credentials.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds + ) as mock: + yield mock + + +@pytest.fixture(autouse=True) +async def mock_token_write(hass: HomeAssistant) -> None: + """Fixture to avoid writing token files to disk.""" + with patch( + "homeassistant.components.google.os.path.isfile", return_value=True + ), patch("homeassistant.components.google.Storage.put"): + yield + + +@pytest.fixture +async def mock_token_read( + hass: HomeAssistant, + creds: OAuth2Credentials, +) -> None: + """Fixture to populate an existing token file.""" + with patch("homeassistant.components.google.Storage.get", return_value=creds): yield -async def test_setup_component(hass, google_setup): - """Test setup component.""" - config = {"google": {CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret"}} +@pytest.fixture +async def calendars_config() -> list[dict[str, Any]]: + """Fixture for tests to override default calendar configuration.""" + return [ + { + "cal_id": CALENDAR_ID, + "entities": [ + { + "device_id": "backyard_light", + "name": "Backyard Light", + "search": "#Backyard", + "track": True, + } + ], + } + ] + + +@pytest.fixture +async def mock_calendars_yaml( + hass: HomeAssistant, + calendars_config: list[dict[str, Any]], +) -> None: + """Fixture that prepares the calendars.yaml file.""" + mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) + with patch("homeassistant.components.google.open", mocked_open_function): + yield + + +@pytest.fixture +async def mock_notification() -> YieldFixture[Mock]: + """Fixture for capturing persistent notifications.""" + with patch("homeassistant.components.persistent_notification.create") as mock: + yield mock + + +@pytest.fixture +async def config() -> dict[str, Any]: + """Fixture for overriding component config.""" + return {DOMAIN: {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-ecret"}} + + +@pytest.fixture +async def component_setup( + hass: HomeAssistant, config: dict[str, Any] +) -> ComponentSetup: + """Fixture for setting up the integration.""" + + async def _setup_func() -> bool: + result = await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + return result + + return _setup_func + + +@pytest.fixture +async def google_service() -> YieldFixture[GoogleCalendarService]: + """Fixture to capture service calls.""" + with patch("homeassistant.components.google.GoogleCalendarService") as mock, patch( + "homeassistant.components.google.calendar.GoogleCalendarService", mock + ): + yield mock + + +async def fire_alarm(hass, point_in_time): + """Fire an alarm and wait for callbacks to run.""" + with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): + async_fire_time_changed(hass, point_in_time) + await hass.async_block_till_done() + + +@pytest.mark.parametrize("config", [{}]) +async def test_setup_config_empty( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_notification: Mock, +): + """Test setup component with an empty configuruation.""" + assert await component_setup() + + mock_notification.assert_not_called() + + assert not hass.states.get("calendar.backyard_light") + + +async def test_init_success( + hass: HomeAssistant, + google_service: GoogleCalendarService, + mock_code_flow: Mock, + mock_exchange: Mock, + mock_notification: Mock, + mock_calendars_yaml: None, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + + state = hass.states.get("calendar.backyard_light") + assert state + assert state.name == "Backyard Light" + assert state.state == STATE_OFF + + mock_notification.assert_called() + assert "We are all setup now" in mock_notification.call_args[0][1] + + +async def test_code_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, + mock_notification: Mock, +) -> None: + """Test loading the integration with no existing credentials.""" + + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=OAuth2DeviceCodeError("Test Failure"), + ): + assert await component_setup() + + assert not hass.states.get("calendar.backyard_light") + + mock_notification.assert_called() + assert "Error: Test Failure" in mock_notification.call_args[0][1] - assert await async_setup_component(hass, "google", config) +@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) +async def test_expired_after_exchange( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, + mock_notification: Mock, +) -> None: + """Test loading the integration with no existing credentials.""" -async def test_get_calendar_info(hass, test_calendar): - """Test getting the calendar info.""" - calendar_info = await hass.async_add_executor_job( - google.get_calendar_info, hass, test_calendar + assert await component_setup() + + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + + assert not hass.states.get("calendar.backyard_light") + + mock_notification.assert_called() + assert ( + "Authentication code expired, please restart Home-Assistant and try again" + in mock_notification.call_args[0][1] ) - assert calendar_info == { - "cal_id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", - "entities": [ + + +async def test_exchange_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, + mock_notification: Mock, +) -> None: + """Test an error while exchanging the code for credentials.""" + + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + side_effect=FlowExchangeError(), + ): + assert await component_setup() + + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + + assert not hass.states.get("calendar.backyard_light") + + mock_notification.assert_called() + assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1] + + +async def test_existing_token( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + mock_calendars_yaml: None, + mock_notification: Mock, +) -> None: + """Test setup with an existing token file.""" + assert await component_setup() + + state = hass.states.get("calendar.backyard_light") + assert state + assert state.name == "Backyard Light" + assert state.state == STATE_OFF + + mock_notification.assert_not_called() + + +@pytest.mark.parametrize( + "token_scopes", ["https://www.googleapis.com/auth/calendar.readonly"] +) +async def test_existing_token_missing_scope( + hass: HomeAssistant, + token_scopes: list[str], + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + mock_calendars_yaml: None, + mock_notification: Mock, + mock_code_flow: Mock, + mock_exchange: Mock, +) -> None: + """Test setup where existing token does not have sufficient scopes.""" + assert await component_setup() + + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + assert len(mock_exchange.mock_calls) == 1 + + state = hass.states.get("calendar.backyard_light") + assert state + assert state.name == "Backyard Light" + assert state.state == STATE_OFF + + # No notifications on success + mock_notification.assert_called() + assert "We are all setup now" in mock_notification.call_args[0][1] + + +@pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]]) +async def test_calendar_yaml_missing_required_fields( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + calendars_config: list[dict[str, Any]], + mock_calendars_yaml: None, + mock_notification: Mock, +) -> None: + """Test setup with a missing schema fields, ignores the error and continues.""" + assert await component_setup() + + assert not hass.states.get("calendar.backyard_light") + + mock_notification.assert_not_called() + + +@pytest.mark.parametrize("calendars_config", [[{"missing-cal_id": "invalid-schema"}]]) +async def test_invalid_calendar_yaml( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + calendars_config: list[dict[str, Any]], + mock_calendars_yaml: None, + mock_notification: Mock, +) -> None: + """Test setup with missing entity id fields fails to setup the integration.""" + + # Integration fails to setup + assert not await component_setup() + + assert not hass.states.get("calendar.backyard_light") + + mock_notification.assert_not_called() + + +async def test_found_calendar_from_api( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + mock_calendars_list: ApiResult, + test_calendar: dict[str, Any], +) -> None: + """Test finding a calendar from the API.""" + + mock_calendars_list({"items": [test_calendar]}) + + mocked_open_function = mock_open(read_data=yaml.dump([])) + with patch("homeassistant.components.google.open", mocked_open_function): + assert await component_setup() + + state = hass.states.get("calendar.we_are_we_are_a_test_calendar") + assert state + assert state.name == "We are, we are, a... Test Calendar" + assert state.state == STATE_OFF + + +async def test_add_event( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + mock_calendars_list: ApiResult, + test_calendar: dict[str, Any], + mock_insert_event: Mock, +) -> None: + """Test service call that adds an event.""" + + assert await component_setup() + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + }, + blocking=True, + ) + mock_insert_event.assert_called() + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": {}, + "end": {}, + }, + ) + + +@pytest.mark.parametrize( + "date_fields,start_timedelta,end_timedelta", + [ + ( + {"in": {"days": 3}}, + datetime.timedelta(days=3), + datetime.timedelta(days=4), + ), + ( + {"in": {"weeks": 1}}, + datetime.timedelta(days=7), + datetime.timedelta(days=8), + ), + ( { - "device_id": "we_are_we_are_a_test_calendar", - "name": "We are, we are, a... Test Calendar", - "track": True, - "ignore_availability": True, - } - ], - } - - -async def test_found_calendar(hass, google_setup, mock_next_event, test_calendar): - """Test when a calendar is found.""" - config = { - "google": { - CONF_CLIENT_ID: "id", - CONF_CLIENT_SECRET: "secret", - "track_new_calendar": True, - } - } - assert await async_setup_component(hass, "google", config) - assert hass.data[google.DATA_INDEX] == {} + "start_date": datetime.date.today().isoformat(), + "end_date": ( + datetime.date.today() + datetime.timedelta(days=2) + ).isoformat(), + }, + datetime.timedelta(days=0), + datetime.timedelta(days=2), + ), + ], + ids=["in_days", "in_weeks", "explit_date"], +) +async def test_add_event_date_ranges( + hass: HomeAssistant, + mock_token_read: None, + calendars_config: list[dict[str, Any]], + component_setup: ComponentSetup, + google_service: GoogleCalendarService, + mock_calendars_list: ApiResult, + test_calendar: dict[str, Any], + mock_insert_event: Mock, + date_fields: dict[str, Any], + start_timedelta: datetime.timedelta, + end_timedelta: datetime.timedelta, +) -> None: + """Test service call that adds an event with various time ranges.""" + + assert await component_setup() await hass.services.async_call( - "google", google.SERVICE_FOUND_CALENDARS, test_calendar, blocking=True + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + **date_fields, + }, + blocking=True, ) + mock_insert_event.assert_called() - assert hass.data[google.DATA_INDEX].get(test_calendar["id"]) is not None + now = datetime.datetime.now() + start_date = now + start_timedelta + end_date = now + end_timedelta + + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": {"date": start_date.date().isoformat()}, + "end": {"date": end_date.date().isoformat()}, + }, + ) diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 2edd750a6e0b8..423bb1b55d7f4 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -383,8 +383,8 @@ def should_2fa(self, state): "willReportState": False, }, { - "id": "alarm_control_panel.alarm", - "name": {"name": "Alarm"}, + "id": "alarm_control_panel.security", + "name": {"name": "Security"}, "traits": ["action.devices.traits.ArmDisarm"], "type": "action.devices.types.SECURITYSYSTEM", "willReportState": False, diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index ff441e44f25bc..dc29e5df4ab80 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -94,7 +94,9 @@ async def test_config_local_sdk(hass, hass_client): client = await hass_client() + assert config.is_local_connected is False config.async_enable_local_sdk() + assert config.is_local_connected is False resp = await client.post( "/api/webhook/mock-webhook-id", @@ -122,6 +124,14 @@ async def test_config_local_sdk(hass, hass_client): "requestId": "mock-req-id", }, ) + + assert config.is_local_connected is True + with patch( + "homeassistant.components.google_assistant.helpers.utcnow", + return_value=dt.utcnow() + timedelta(seconds=90), + ): + assert config.is_local_connected is False + assert resp.status == HTTPStatus.OK result = await resp.json() assert result["requestId"] == "mock-req-id" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 5690591ccd218..9e09ebb9ff2a7 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -16,7 +16,7 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service -from tests.components.tts.test_init import mutagen_mock # noqa: F401 +from tests.components.tts.conftest import mutagen_mock # noqa: F401 @pytest.fixture(autouse=True) diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 7f668383c4b71..4ca7c5a91055f 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -1,21 +1,27 @@ """Fixtures for Google Time Travel tests.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from googlemaps.exceptions import ApiError import pytest +from homeassistant.components.google_travel_time.const import DOMAIN -@pytest.fixture(name="validate_config_entry") -def validate_config_entry_fixture(): - """Return valid config entry.""" - with patch( - "homeassistant.components.google_travel_time.helpers.Client", - return_value=Mock(), - ), patch( - "homeassistant.components.google_travel_time.helpers.distance_matrix", - return_value=None, - ): - yield +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_config") +async def mock_config_fixture(hass, data, options): + """Mock a Google Travel Time config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options=options, + entry_id="test", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield config_entry @pytest.fixture(name="bypass_setup") @@ -38,21 +44,17 @@ def bypass_platform_setup_fixture(): yield -@pytest.fixture(name="bypass_update") -def bypass_update_fixture(): - """Bypass sensor update.""" - with patch("homeassistant.components.google_travel_time.sensor.distance_matrix"): - yield +@pytest.fixture(name="validate_config_entry") +def validate_config_entry_fixture(): + """Return valid config entry.""" + with patch("homeassistant.components.google_travel_time.helpers.Client"), patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix" + ) as distance_matrix_mock: + distance_matrix_mock.return_value = None + yield distance_matrix_mock @pytest.fixture(name="invalidate_config_entry") -def invalidate_config_entry_fixture(): +def invalidate_config_entry_fixture(validate_config_entry): """Return invalid config entry.""" - with patch( - "homeassistant.components.google_travel_time.helpers.Client", - return_value=Mock(), - ), patch( - "homeassistant.components.google_travel_time.helpers.distance_matrix", - side_effect=ApiError("test"), - ): - yield + validate_config_entry.side_effect = ApiError("test") diff --git a/tests/components/google_travel_time/const.py b/tests/components/google_travel_time/const.py new file mode 100644 index 0000000000000..844766ceffaa0 --- /dev/null +++ b/tests/components/google_travel_time/const.py @@ -0,0 +1,14 @@ +"""Constants for google_travel_time tests.""" + + +from homeassistant.components.google_travel_time.const import ( + CONF_DESTINATION, + CONF_ORIGIN, +) +from homeassistant.const import CONF_API_KEY + +MOCK_CONFIG = { + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", +} diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 9b615afbbe134..1426c749552f0 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Google Maps Travel Time config flow.""" +import pytest + from homeassistant import config_entries, data_entry_flow from homeassistant.components.google_travel_time.const import ( ARRIVAL_TIME, @@ -25,10 +27,11 @@ CONF_UNIT_SYSTEM_IMPERIAL, ) -from tests.common import MockConfigEntry +from tests.components.google_travel_time.const import MOCK_CONFIG -async def test_minimum_fields(hass, validate_config_entry, bypass_setup): +@pytest.mark.usefixtures("validate_config_entry", "bypass_setup") +async def test_minimum_fields(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -38,11 +41,7 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - }, + MOCK_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -55,7 +54,8 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): } -async def test_invalid_config_entry(hass, invalidate_config_entry): +@pytest.mark.usefixtures("invalidate_config_entry") +async def test_invalid_config_entry(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -64,37 +64,32 @@ async def test_invalid_config_entry(hass, invalidate_config_entry): assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - }, + MOCK_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} -async def test_options_flow(hass, validate_config_entry, bypass_update): +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_MODE: "driving", + CONF_ARRIVAL_TIME: "test", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + }, + ) + ], +) +@pytest.mark.usefixtures("validate_config_entry") +async def test_options_flow(hass, mock_config): """Test options flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - }, - options={ - CONF_MODE: "driving", - CONF_ARRIVAL_TIME: "test", - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - }, + result = await hass.config_entries.options.async_init( + mock_config.entry_id, data=None ) - 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, data=None) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" @@ -126,7 +121,7 @@ async def test_options_flow(hass, validate_config_entry, bypass_update): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", } - assert entry.options == { + assert mock_config.options == { CONF_MODE: "driving", CONF_LANGUAGE: "en", CONF_AVOID: "tolls", @@ -138,21 +133,16 @@ async def test_options_flow(hass, validate_config_entry, bypass_update): } -async def test_options_flow_departure_time(hass, validate_config_entry, bypass_update): - """Test options flow wiith departure time.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "api_key", - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - }, +@pytest.mark.parametrize( + "data,options", + [(MOCK_CONFIG, {})], +) +@pytest.mark.usefixtures("validate_config_entry") +async def test_options_flow_departure_time(hass, mock_config): + """Test options flow with departure time.""" + result = await hass.config_entries.options.async_init( + mock_config.entry_id, data=None ) - 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, data=None) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" @@ -184,7 +174,7 @@ async def test_options_flow_departure_time(hass, validate_config_entry, bypass_u CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", } - assert entry.options == { + assert mock_config.options == { CONF_MODE: "driving", CONF_LANGUAGE: "en", CONF_AVOID: "tolls", @@ -196,7 +186,8 @@ async def test_options_flow_departure_time(hass, validate_config_entry, bypass_u } -async def test_dupe(hass, validate_config_entry, bypass_setup): +@pytest.mark.usefixtures("validate_config_entry", "bypass_setup") +async def test_dupe(hass): """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py new file mode 100644 index 0000000000000..daedcfef4c18f --- /dev/null +++ b/tests/components/google_travel_time/test_sensor.py @@ -0,0 +1,220 @@ +"""Test the Google Maps Travel Time sensors.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.google_travel_time.const import ( + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_TRAVEL_MODE, + DOMAIN, +) + +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_update") +def mock_update_fixture(): + """Mock an update to the sensor.""" + with patch("homeassistant.components.google_travel_time.sensor.Client"), patch( + "homeassistant.components.google_travel_time.sensor.distance_matrix" + ) as distance_matrix_mock: + distance_matrix_mock.return_value = { + "rows": [ + { + "elements": [ + { + "duration_in_traffic": { + "value": 1620, + "text": "27 mins", + }, + "duration": { + "value": 1560, + "text": "26 mins", + }, + "distance": {"text": "21.3 km"}, + } + ] + } + ] + } + yield distance_matrix_mock + + +@pytest.fixture(name="mock_update_duration") +def mock_update_duration_fixture(mock_update): + """Mock an update to the sensor returning no duration_in_traffic.""" + mock_update.return_value = { + "rows": [ + { + "elements": [ + { + "duration": { + "value": 1560, + "text": "26 mins", + }, + "distance": {"text": "21.3 km"}, + } + ] + } + ] + } + yield mock_update + + +@pytest.fixture(name="mock_update_empty") +def mock_update_empty_fixture(mock_update): + """Mock an update to the sensor with an empty response.""" + mock_update.return_value = None + yield mock_update + + +@pytest.mark.parametrize( + "data,options", + [(MOCK_CONFIG, {})], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor(hass): + """Test that sensor works.""" + assert hass.states.get("sensor.google_travel_time").state == "27" + assert ( + hass.states.get("sensor.google_travel_time").attributes["attribution"] + == "Powered by Google" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["duration"] == "26 mins" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["duration_in_traffic"] + == "27 mins" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["distance"] == "21.3 km" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["origin"] == "location1" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["destination"] + == "location2" + ) + assert ( + hass.states.get("sensor.google_travel_time").attributes["unit_of_measurement"] + == "min" + ) + + +@pytest.mark.parametrize( + "data,options", + [(MOCK_CONFIG, {})], +) +@pytest.mark.usefixtures("mock_update_duration", "mock_config") +async def test_sensor_duration(hass): + """Test that sensor works with no duration_in_traffic in response.""" + assert hass.states.get("sensor.google_travel_time").state == "26" + + +@pytest.mark.parametrize( + "data,options", + [(MOCK_CONFIG, {})], +) +@pytest.mark.usefixtures("mock_update_empty", "mock_config") +async def test_sensor_empty_response(hass): + """Test that sensor works for an empty response.""" + assert hass.states.get("sensor.google_travel_time").state == "unknown" + + +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_DEPARTURE_TIME: "10:00", + }, + ), + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor_departure_time(hass): + """Test that sensor works for departure time.""" + assert hass.states.get("sensor.google_travel_time").state == "27" + + +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_DEPARTURE_TIME: "custom_timestamp", + }, + ), + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor_departure_time_custom_timestamp(hass): + """Test that sensor works for departure time with a custom timestamp.""" + assert hass.states.get("sensor.google_travel_time").state == "27" + + +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_ARRIVAL_TIME: "10:00", + }, + ), + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor_arrival_time(hass): + """Test that sensor works for arrival time.""" + assert hass.states.get("sensor.google_travel_time").state == "27" + + +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_ARRIVAL_TIME: "custom_timestamp", + }, + ), + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor_arrival_time_custom_timestamp(hass): + """Test that sensor works for arrival time with a custom timestamp.""" + assert hass.states.get("sensor.google_travel_time").state == "27" + + +@pytest.mark.usefixtures("mock_update") +async def test_sensor_deprecation_warning(hass, caplog): + """Test that sensor setup prints a deprecating warning for old configs. + + The mock_config fixture does not work with caplog. + """ + data = MOCK_CONFIG.copy() + data[CONF_TRAVEL_MODE] = "driving" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + entry_id="test", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.google_travel_time").state == "27" + wstr = ( + "Google Travel Time: travel_mode is deprecated, please " + "add mode to the options dictionary instead!" + ) + assert wstr in caplog.text diff --git a/tests/components/greeneye_monitor/common.py b/tests/components/greeneye_monitor/common.py index 0a19b79795f6b..e9285647f4de2 100644 --- a/tests/components/greeneye_monitor/common.py +++ b/tests/components/greeneye_monitor/common.py @@ -239,3 +239,13 @@ def mock_monitor(serial_number: int) -> MagicMock: monitor.temperature_sensors = [mock_temperature_sensor() for i in range(0, 8)] monitor.channels = [mock_channel() for i in range(0, 32)] return monitor + + +async def connect_monitor( + hass: HomeAssistant, monitors: AsyncMock, serial_number: int +) -> MagicMock: + """Simulate a monitor connecting to Home Assistant. Returns the mock monitor API object.""" + monitor = mock_monitor(serial_number) + monitors.add_monitor(monitor) + await hass.async_block_till_done() + return monitor diff --git a/tests/components/greeneye_monitor/test_init.py b/tests/components/greeneye_monitor/test_init.py index c8e1371493958..df8c67e7eed97 100644 --- a/tests/components/greeneye_monitor/test_init.py +++ b/tests/components/greeneye_monitor/test_init.py @@ -18,6 +18,7 @@ SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS, SINGLE_MONITOR_SERIAL_NUMBER, + connect_monitor, setup_greeneye_monitor_component_with_config, ) from .conftest import ( @@ -53,7 +54,7 @@ async def test_setup_creates_temperature_entities( assert await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS ) - + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_temperature_sensor_registered( hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "temp_a" ) @@ -87,7 +88,7 @@ async def test_setup_creates_pulse_counter_entities( assert await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS ) - + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_pulse_counter_registered( hass, SINGLE_MONITOR_SERIAL_NUMBER, @@ -124,7 +125,7 @@ async def test_setup_creates_power_sensor_entities( assert await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS ) - + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "channel 1") assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "channel two") @@ -136,7 +137,7 @@ async def test_setup_creates_voltage_sensor_entities( assert await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_voltage_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "voltage 1") @@ -147,6 +148,10 @@ async def test_multi_monitor_config(hass: HomeAssistant, monitors: AsyncMock) -> MULTI_MONITOR_CONFIG, ) + await connect_monitor(hass, monitors, 1) + await connect_monitor(hass, monitors, 2) + await connect_monitor(hass, monitors, 3) + assert_temperature_sensor_registered(hass, 1, 1, "unit_1_temp_1") assert_temperature_sensor_registered(hass, 2, 1, "unit_2_temp_1") assert_temperature_sensor_registered(hass, 3, 1, "unit_3_temp_1") diff --git a/tests/components/greeneye_monitor/test_sensor.py b/tests/components/greeneye_monitor/test_sensor.py index ac1fe92873a58..f739b8a64cad0 100644 --- a/tests/components/greeneye_monitor/test_sensor.py +++ b/tests/components/greeneye_monitor/test_sensor.py @@ -1,5 +1,5 @@ """Tests for greeneye_monitor sensors.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock from homeassistant.components.greeneye_monitor.sensor import ( DATA_PULSES, @@ -19,38 +19,50 @@ SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS, SINGLE_MONITOR_SERIAL_NUMBER, - mock_monitor, + connect_monitor, setup_greeneye_monitor_component_with_config, ) from .conftest import assert_sensor_state -async def test_disable_sensor_before_monitor_connected( +async def test_sensor_does_not_exist_before_monitor_connected( hass: HomeAssistant, monitors: AsyncMock ) -> None: - """Test that a sensor disabled before its monitor connected stops listening for new monitors.""" + """Test that a sensor does not exist before its monitor is connected.""" # The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) + entity_registry = get_entity_registry(hass) + assert entity_registry.async_get("sensor.voltage_1") is None + + +async def test_sensors_created_when_monitor_connected( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that sensors get created when the monitor first connects.""" + # The sensor base class handles updating the state on connection, so we test this with a single voltage sensor for ease + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS + ) + assert len(monitors.listeners) == 1 - await disable_entity(hass, "sensor.voltage_1") + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener + assert_sensor_state(hass, "sensor.voltage_1", "120.0") -async def test_updates_state_when_monitor_connected( +async def test_sensors_created_during_setup_if_monitor_already_connected( hass: HomeAssistant, monitors: AsyncMock ) -> None: - """Test that a sensor updates its state when its monitor first connects.""" + """Test that sensors get created during setup if the monitor happens to connect really quickly.""" # The sensor base class handles updating the state on connection, so we test this with a single voltage sensor for ease + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - assert_sensor_state(hass, "sensor.voltage_1", STATE_UNKNOWN) - assert len(monitors.listeners) == 1 - connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener assert_sensor_state(hass, "sensor.voltage_1", "120.0") @@ -63,7 +75,7 @@ async def test_disable_sensor_after_monitor_connected( await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + monitor = await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert len(monitor.voltage_sensor.listeners) == 1 await disable_entity(hass, "sensor.voltage_1") @@ -78,7 +90,7 @@ async def test_updates_state_when_sensor_pushes( await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + monitor = await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_sensor_state(hass, "sensor.voltage_1", "120.0") monitor.voltage_sensor.voltage = 119.8 @@ -93,7 +105,7 @@ async def test_power_sensor_initially_unknown( await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS ) - connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_sensor_state( hass, "sensor.channel_1", STATE_UNKNOWN, {DATA_WATT_SECONDS: 1000} ) @@ -109,7 +121,7 @@ async def test_power_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS ) - monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + monitor = await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) monitor.channels[0].watts = 120.0 monitor.channels[1].watts = 120.0 monitor.channels[0].notify_all_listeners() @@ -120,12 +132,35 @@ async def test_power_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: assert_sensor_state(hass, "sensor.channel_two", "120.0", {DATA_WATT_SECONDS: -400}) +async def test_pulse_counter_initially_unknown( + hass: HomeAssistant, monitors: AsyncMock +) -> None: + """Test that the pulse counter sensor can handle its initial state being unknown (since the GEM API needs at least two packets to arrive before it can compute pulses per time).""" + await setup_greeneye_monitor_component_with_config( + hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS + ) + monitor = await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) + monitor.pulse_counters[0].pulses_per_second = None + monitor.pulse_counters[1].pulses_per_second = None + monitor.pulse_counters[2].pulses_per_second = None + monitor.pulse_counters[0].notify_all_listeners() + monitor.pulse_counters[1].notify_all_listeners() + monitor.pulse_counters[2].notify_all_listeners() + assert_sensor_state(hass, "sensor.pulse_a", STATE_UNKNOWN, {DATA_PULSES: 1000}) + # This counter was configured with each pulse meaning 0.5 gallons and + # wanting to show gallons per minute, so 10 pulses per second -> 300 gal/min + assert_sensor_state(hass, "sensor.pulse_2", STATE_UNKNOWN, {DATA_PULSES: 1000}) + # This counter was configured with each pulse meaning 0.5 gallons and + # wanting to show gallons per hour, so 10 pulses per second -> 18000 gal/hr + assert_sensor_state(hass, "sensor.pulse_3", STATE_UNKNOWN, {DATA_PULSES: 1000}) + + async def test_pulse_counter(hass: HomeAssistant, monitors: AsyncMock) -> None: """Test that a pulse counter sensor reports its values properly, including calculating different units.""" await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS ) - connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_sensor_state(hass, "sensor.pulse_a", "10.0", {DATA_PULSES: 1000}) # This counter was configured with each pulse meaning 0.5 gallons and # wanting to show gallons per minute, so 10 pulses per second -> 300 gal/min @@ -140,7 +175,7 @@ async def test_temperature_sensor(hass: HomeAssistant, monitors: AsyncMock) -> N await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS ) - connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) # The config says that the sensor is reporting in Fahrenheit; if we set that up # properly, HA will have converted that to Celsius by default. assert_sensor_state(hass, "sensor.temp_a", "0.0") @@ -151,28 +186,21 @@ async def test_voltage_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None: await setup_greeneye_monitor_component_with_config( hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS ) - connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER) + await connect_monitor(hass, monitors, SINGLE_MONITOR_SERIAL_NUMBER) assert_sensor_state(hass, "sensor.voltage_1", "120.0") async def test_multi_monitor_sensors(hass: HomeAssistant, monitors: AsyncMock) -> None: """Test that sensors still work when multiple monitors are registered.""" await setup_greeneye_monitor_component_with_config(hass, MULTI_MONITOR_CONFIG) - connect_monitor(monitors, 1) - connect_monitor(monitors, 2) - connect_monitor(monitors, 3) + await connect_monitor(hass, monitors, 1) + await connect_monitor(hass, monitors, 2) + await connect_monitor(hass, monitors, 3) assert_sensor_state(hass, "sensor.unit_1_temp_1", "32.0") assert_sensor_state(hass, "sensor.unit_2_temp_1", "0.0") assert_sensor_state(hass, "sensor.unit_3_temp_1", "32.0") -def connect_monitor(monitors: AsyncMock, serial_number: int) -> MagicMock: - """Simulate a monitor connecting to Home Assistant. Returns the mock monitor API object.""" - monitor = mock_monitor(serial_number) - monitors.add_monitor(monitor) - return monitor - - async def disable_entity(hass: HomeAssistant, entity_id: str) -> None: """Disable the given entity.""" entity_registry = get_entity_registry(hass) diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 0a85c793aaa33..a0872b11f16a1 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -95,6 +95,16 @@ async def test_state_reporting_all(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + async def test_state_reporting_any(hass): """Test the state reporting.""" @@ -116,11 +126,10 @@ async def test_state_reporting_any(hass): await hass.async_start() await hass.async_block_till_done() - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_OFF) @@ -137,7 +146,6 @@ async def test_state_reporting_any(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -149,3 +157,13 @@ async def test_state_reporting_any(hass): entry = entity_registry.async_get("binary_sensor.binary_sensor_group") assert entry assert entry.unique_id == "unique_identifier" + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index fc3157289e970..a56aa6355e8df 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -74,6 +74,7 @@ async def test_step_zeroconf(hass, setup_guardian): """Test the zeroconf step.""" zeroconf_data = zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], port=7777, hostname="GVC1-ABCD.local.", type="_api._udp.local.", @@ -103,6 +104,7 @@ async def test_step_zeroconf_already_in_progress(hass): """Test the zeroconf step aborting because it's already in progress.""" zeroconf_data = zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], port=7777, hostname="GVC1-ABCD.local.", type="_api._udp.local.", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e006cf9d82913..689ec13804361 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -397,12 +397,18 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): await hass.services.async_call( "hassio", "backup_partial", - {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, + { + "homeassistant": True, + "addons": ["test"], + "folders": ["ssl"], + "password": "123456", + }, ) await hass.async_block_till_done() assert aioclient_mock.call_count == 12 assert aioclient_mock.mock_calls[-1][2] == { + "homeassistant": True, "addons": ["test"], "folders": ["ssl"], "password": "123456", diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index c1ce1ffaddba3..b520eb7f87448 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -567,8 +567,8 @@ async def test_fan_restore(hass, hk_driver, events): assert acc.char_swing is not None -async def test_fan_preset_modes(hass, hk_driver, events): - """Test fan with direction.""" +async def test_fan_multiple_preset_modes(hass, hk_driver, events): + """Test fan with multiple preset modes.""" entity_id = "fan.demo" hass.states.async_set( @@ -645,3 +645,84 @@ async def test_fan_preset_modes(hass, hk_driver, events): assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert events[-1].data["service"] == "turn_on" assert len(events) == 2 + + +async def test_fan_single_preset_mode(hass, hk_driver, events): + """Test fan with a single preset mode.""" + entity_id = "fan.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 42, + ATTR_PRESET_MODE: "smart", + ATTR_PRESET_MODES: ["smart"], + }, + ) + await hass.async_block_till_done() + acc = Fan(hass, hk_driver, "Fan", entity_id, 1, None) + hk_driver.add_accessory(acc) + + assert acc.char_target_fan_state.value == 1 + + await acc.run() + await hass.async_block_till_done() + + # Set from HomeKit + call_set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + char_target_fan_state_iid = acc.char_target_fan_state.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_PERCENTAGE] == 42 + assert len(events) == 1 + assert events[-1].data["service"] == "turn_on" + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_iid, + HAP_REPR_VALUE: 1, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_set_preset_mode[0] + assert call_set_preset_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_preset_mode[0].data[ATTR_PRESET_MODE] == "smart" + assert events[-1].data["service"] == "set_preset_mode" + assert len(events) == 2 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 42, + ATTR_PRESET_MODE: None, + ATTR_PRESET_MODES: ["smart"], + }, + ) + await hass.async_block_till_done() + assert acc.char_target_fan_state.value == 0 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index a11aa9d6cb75a..d1db618e7e4bc 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -7,12 +7,16 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_SWING_MODE, + ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, @@ -24,6 +28,12 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_HUMIDITY, DOMAIN as DOMAIN_CLIMATE, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -31,11 +41,22 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + SERVICE_SET_FAN_MODE, + SERVICE_SET_SWING_MODE, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, + CHAR_CURRENT_FAN_STATE, + CHAR_ROTATION_SPEED, + CHAR_SWING_MODE, + CHAR_TARGET_FAN_STATE, DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, @@ -2017,3 +2038,314 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, events): assert acc.char_target_heat_cool.value == 3 assert acc.char_current_temp.value == 1000 assert acc.char_display_units.value == 0 + + +async def test_thermostat_with_fan_modes_with_auto(hass, hk_driver, events): + """Test a thermostate with fan modes with an auto fan mode.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [FAN_LOW, FAN_MEDIUM, FAN_HIGH] + assert CHAR_ROTATION_SPEED in acc.fan_chars + assert CHAR_TARGET_FAN_STATE in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + assert acc.char_speed.value == 100 + + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_LOW, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + assert acc.char_speed.value == pytest.approx(100 / 3) + + call_set_swing_mode = async_mock_service( + hass, DOMAIN_CLIMATE, SERVICE_SET_SWING_MODE + ) + char_swing_iid = acc.char_swing.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_swing_iid, + HAP_REPR_VALUE: 0, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_swing_mode) == 1 + assert call_set_swing_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_swing_mode[-1].data[ATTR_SWING_MODE] == SWING_OFF + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_swing_iid, + HAP_REPR_VALUE: 1, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_swing_mode) == 2 + assert call_set_swing_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_swing_mode[-1].data[ATTR_SWING_MODE] == SWING_BOTH + + call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE) + char_rotation_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_rotation_speed_iid, + HAP_REPR_VALUE: 100, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 1 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_HIGH + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_rotation_speed_iid, + HAP_REPR_VALUE: 100 / 3, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 2 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_LOW + + char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID] + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_active_iid, + HAP_REPR_VALUE: 0, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + char_target_fan_state_iid = acc.char_target_fan_state.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_iid, + HAP_REPR_VALUE: 1, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 3 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_AUTO + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_fan_state_iid, + HAP_REPR_VALUE: 0, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 4 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_MEDIUM + + +async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): + """Test a thermostate with fan modes that can turn off.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: [FAN_ON, FAN_OFF], + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_ON, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + assert acc.char_active.value == 1 + + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: [FAN_ON, FAN_OFF], + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_OFF, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + call_set_fan_mode = async_mock_service(hass, DOMAIN_CLIMATE, SERVICE_SET_FAN_MODE) + char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID] + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_active_iid, + HAP_REPR_VALUE: 1, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 1 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_ON + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_active_iid, + HAP_REPR_VALUE: 0, + } + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert len(call_set_fan_mode) == 2 + assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index ef77d5bc65246..8f59fae8639f2 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -10,9 +10,7 @@ from unittest import mock from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.services import ServicesTypes -from aiohomekit.testing import FakeController +from aiohomekit.testing import FakeController, FakePairing from homeassistant.components import zeroconf from homeassistant.components.device_automation import DeviceAutomationType @@ -24,7 +22,8 @@ IDENTIFIER_ACCESSORY_ID, IDENTIFIER_SERIAL_NUMBER, ) -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component @@ -94,7 +93,14 @@ class DeviceTestInfo: class Helper: """Helper methods for interacting with HomeKit fakes.""" - def __init__(self, hass, entity_id, pairing, accessory, config_entry): + def __init__( + self, + hass: HomeAssistant, + entity_id: str, + pairing: FakePairing, + accessory: Accessory, + config_entry: ConfigEntry, + ) -> None: """Create a helper for a given accessory/entity.""" self.hass = hass self.entity_id = entity_id @@ -102,19 +108,43 @@ def __init__(self, hass, entity_id, pairing, accessory, config_entry): self.accessory = accessory self.config_entry = config_entry - self.characteristics = {} - for service in self.accessory.services: - service_name = ServicesTypes.get_short(service.type) - for char in service.characteristics: - char_name = CharacteristicsTypes.get_short(char.type) - self.characteristics[(service_name, char_name)] = char + async def async_update( + self, service: str, characteristics: dict[str, Any] + ) -> State: + """Set the characteristics on this service.""" + changes = [] + + service = self.accessory.services.first(service_type=service) + aid = service.accessory.aid + + for ctype, value in characteristics.items(): + char = service.characteristics.first(char_types=[ctype]) + changes.append((aid, char.iid, value)) + + self.pairing.testing.update_aid_iid(changes) + + if not self.pairing.testing.events_enabled: + # If events aren't enabled, explicitly do a poll + # If they are enabled, then HA will pick up the changes next time + # we yield control + await time_changed(self.hass, 60) - async def update_named_service(self, service, characteristics): - """Update a service.""" - self.pairing.testing.update_named_service(service, characteristics) await self.hass.async_block_till_done() - async def poll_and_get_state(self): + state = self.hass.states.get(self.entity_id) + assert state is not None + return state + + @callback + def async_assert_service_values( + self, service: str, characteristics: dict[str, Any] + ) -> None: + """Assert a service has characteristics with these values.""" + service = self.accessory.services.first(service_type=service) + for ctype, value in characteristics.items(): + assert service.value(ctype) == value + + 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) @@ -144,7 +174,9 @@ async def setup_platform(hass): """Load the platform but with a fake Controller API.""" config = {"discovery": {}} - with mock.patch("aiohomekit.Controller") as controller: + with mock.patch( + "homeassistant.components.homekit_controller.utils.Controller" + ) as controller: fake_controller = controller.return_value = FakeController() await async_setup_component(hass, DOMAIN, config) @@ -190,6 +222,7 @@ async def device_config_changed(hass, accessories): discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", + addresses=["127.0.0.1"], hostname="mock_hostname", name="TestDevice", port=8080, @@ -230,7 +263,7 @@ async def setup_test_component(hass, setup_accessory, capitalize=False, suffix=N domain = None for service in accessory.services: - service_name = ServicesTypes.get_short(service.type) + service_name = service.type if service_name in HOMEKIT_ACCESSORY_DISPATCH: domain = HOMEKIT_ACCESSORY_DISPATCH[service_name] break diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 46b8a5de3e70b..81688f88a4b50 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -27,7 +27,10 @@ def utcnow(request): def controller(hass): """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with unittest.mock.patch("aiohomekit.Controller", return_value=instance): + with unittest.mock.patch( + "homeassistant.components.homekit_controller.utils.Controller", + return_value=instance, + ): yield instance diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 2d540f31850c3..3c47195b44221 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -129,6 +129,13 @@ async def test_ecobee3_setup(hass): unit_of_measurement=TEMP_CELSIUS, state="21.8", ), + EntityTestInfo( + entity_id="select.homew_current_mode", + friendly_name="HomeW Current Mode", + unique_id="homekit-123456789012-aid:1-sid:16-cid:33", + capabilities={"options": ["home", "sleep", "away"]}, + state="home", + ), ], ), ) diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 9591eb27b6ffa..33a1ebdbafefb 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -4,6 +4,7 @@ from unittest import mock from aiohomekit.exceptions import AccessoryDisconnectedError, EncryptionError +from aiohomekit.model import CharacteristicsTypes, ServicesTypes from aiohomekit.testing import FakePairing import pytest @@ -72,26 +73,29 @@ async def test_recover_from_failure(hass, utcnow, failure_cls): accessories = await setup_accessories_from_file(hass, "koogeek_ls1.json") config_entry, pairing = await setup_test_accessories(hass, accessories) + pairing.testing.events_enabled = False + helper = Helper( hass, "light.koogeek_ls1_20833f", pairing, accessories[0], config_entry ) # Set light state on fake device to off - helper.characteristics[LIGHT_ON].set_value(False) + state = await helper.async_update( + ServicesTypes.LIGHTBULB, {CharacteristicsTypes.ON: False} + ) # Test that entity starts off in a known state - state = await helper.poll_and_get_state() assert state.state == "off" - # Set light state on fake device to on - helper.characteristics[LIGHT_ON].set_value(True) - # Test that entity remains in the same state if there is a network error next_update = dt_util.utcnow() + timedelta(seconds=60) with mock.patch.object(FakePairing, "get_characteristics") as get_char: get_char.side_effect = failure_cls("Disconnected") - state = await helper.poll_and_get_state() + # Set light state on fake device to on + state = await helper.async_update( + ServicesTypes.LIGHTBULB, {CharacteristicsTypes.ON: True} + ) assert state.state == "off" chars = get_char.call_args[0][0] @@ -102,5 +106,7 @@ async def test_recover_from_failure(hass, utcnow, failure_cls): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHTBULB, {CharacteristicsTypes.ON: True} + ) assert state.state == "on" diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 5694be5f955fa..2804ffff82489 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -4,9 +4,6 @@ from tests.components.homekit_controller.common import setup_test_component -CURRENT_STATE = ("security-system", "security-system-state.current") -TARGET_STATE = ("security-system", "security-system-state.target") - def create_security_system_service(accessory): """Define a security-system characteristics as per page 219 of HAP spec.""" @@ -36,7 +33,12 @@ async def test_switch_change_alarm_state(hass, utcnow): {"entity_id": "alarm_control_panel.testdevice"}, blocking=True, ) - assert helper.characteristics[TARGET_STATE].value == 0 + helper.async_assert_service_values( + ServicesTypes.SECURITY_SYSTEM, + { + CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: 0, + }, + ) await hass.services.async_call( "alarm_control_panel", @@ -44,7 +46,12 @@ async def test_switch_change_alarm_state(hass, utcnow): {"entity_id": "alarm_control_panel.testdevice"}, blocking=True, ) - assert helper.characteristics[TARGET_STATE].value == 1 + helper.async_assert_service_values( + ServicesTypes.SECURITY_SYSTEM, + { + CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: 1, + }, + ) await hass.services.async_call( "alarm_control_panel", @@ -52,7 +59,12 @@ async def test_switch_change_alarm_state(hass, utcnow): {"entity_id": "alarm_control_panel.testdevice"}, blocking=True, ) - assert helper.characteristics[TARGET_STATE].value == 2 + helper.async_assert_service_values( + ServicesTypes.SECURITY_SYSTEM, + { + CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: 2, + }, + ) await hass.services.async_call( "alarm_control_panel", @@ -60,30 +72,50 @@ async def test_switch_change_alarm_state(hass, utcnow): {"entity_id": "alarm_control_panel.testdevice"}, blocking=True, ) - assert helper.characteristics[TARGET_STATE].value == 3 + helper.async_assert_service_values( + ServicesTypes.SECURITY_SYSTEM, + { + CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET: 3, + }, + ) async def test_switch_read_alarm_state(hass, utcnow): """Test that we can read the state of a HomeKit alarm accessory.""" helper = await setup_test_component(hass, create_security_system_service) - helper.characteristics[CURRENT_STATE].value = 0 + await helper.async_update( + ServicesTypes.SECURITY_SYSTEM, + {CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT: 0}, + ) state = await helper.poll_and_get_state() assert state.state == "armed_home" assert state.attributes["battery_level"] == 50 - helper.characteristics[CURRENT_STATE].value = 1 + await helper.async_update( + ServicesTypes.SECURITY_SYSTEM, + {CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT: 1}, + ) state = await helper.poll_and_get_state() assert state.state == "armed_away" - helper.characteristics[CURRENT_STATE].value = 2 + await helper.async_update( + ServicesTypes.SECURITY_SYSTEM, + {CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT: 2}, + ) state = await helper.poll_and_get_state() assert state.state == "armed_night" - helper.characteristics[CURRENT_STATE].value = 3 + await helper.async_update( + ServicesTypes.SECURITY_SYSTEM, + {CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT: 3}, + ) state = await helper.poll_and_get_state() assert state.state == "disarmed" - helper.characteristics[CURRENT_STATE].value = 4 + await helper.async_update( + ServicesTypes.SECURITY_SYSTEM, + {CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT: 4}, + ) state = await helper.poll_and_get_state() assert state.state == "triggered" diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index e0b23775c4d5e..d83beb07df369 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -6,13 +6,6 @@ from tests.components.homekit_controller.common import setup_test_component -MOTION_DETECTED = ("motion", "motion-detected") -CONTACT_STATE = ("contact", "contact-state") -SMOKE_DETECTED = ("smoke", "smoke-detected") -CARBON_MONOXIDE_DETECTED = ("carbon-monoxide", "carbon-monoxide.detected") -OCCUPANCY_DETECTED = ("occupancy", "occupancy-detected") -LEAK_DETECTED = ("leak", "leak-detected") - def create_motion_sensor_service(accessory): """Define motion characteristics as per page 225 of HAP spec.""" @@ -26,11 +19,15 @@ async def test_motion_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit motion sensor accessory.""" helper = await setup_test_component(hass, create_motion_sensor_service) - helper.characteristics[MOTION_DETECTED].value = False + await helper.async_update( + ServicesTypes.MOTION_SENSOR, {CharacteristicsTypes.MOTION_DETECTED: False} + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[MOTION_DETECTED].value = True + await helper.async_update( + ServicesTypes.MOTION_SENSOR, {CharacteristicsTypes.MOTION_DETECTED: True} + ) state = await helper.poll_and_get_state() assert state.state == "on" @@ -49,11 +46,15 @@ async def test_contact_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_contact_sensor_service) - helper.characteristics[CONTACT_STATE].value = 0 + await helper.async_update( + ServicesTypes.CONTACT_SENSOR, {CharacteristicsTypes.CONTACT_STATE: 0} + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[CONTACT_STATE].value = 1 + await helper.async_update( + ServicesTypes.CONTACT_SENSOR, {CharacteristicsTypes.CONTACT_STATE: 1} + ) state = await helper.poll_and_get_state() assert state.state == "on" @@ -72,11 +73,15 @@ async def test_smoke_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_smoke_sensor_service) - helper.characteristics[SMOKE_DETECTED].value = 0 + await helper.async_update( + ServicesTypes.SMOKE_SENSOR, {CharacteristicsTypes.SMOKE_DETECTED: 0} + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[SMOKE_DETECTED].value = 1 + await helper.async_update( + ServicesTypes.SMOKE_SENSOR, {CharacteristicsTypes.SMOKE_DETECTED: 1} + ) state = await helper.poll_and_get_state() assert state.state == "on" @@ -95,11 +100,17 @@ async def test_carbon_monoxide_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit contact accessory.""" helper = await setup_test_component(hass, create_carbon_monoxide_sensor_service) - helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 0 + await helper.async_update( + ServicesTypes.CARBON_MONOXIDE_SENSOR, + {CharacteristicsTypes.CARBON_MONOXIDE_DETECTED: 0}, + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[CARBON_MONOXIDE_DETECTED].value = 1 + await helper.async_update( + ServicesTypes.CARBON_MONOXIDE_SENSOR, + {CharacteristicsTypes.CARBON_MONOXIDE_DETECTED: 1}, + ) state = await helper.poll_and_get_state() assert state.state == "on" @@ -118,11 +129,15 @@ async def test_occupancy_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit occupancy sensor accessory.""" helper = await setup_test_component(hass, create_occupancy_sensor_service) - helper.characteristics[OCCUPANCY_DETECTED].value = False + await helper.async_update( + ServicesTypes.OCCUPANCY_SENSOR, {CharacteristicsTypes.OCCUPANCY_DETECTED: False} + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[OCCUPANCY_DETECTED].value = True + await helper.async_update( + ServicesTypes.OCCUPANCY_SENSOR, {CharacteristicsTypes.OCCUPANCY_DETECTED: True} + ) state = await helper.poll_and_get_state() assert state.state == "on" @@ -141,11 +156,15 @@ async def test_leak_sensor_read_state(hass, utcnow): """Test that we can read the state of a HomeKit leak sensor accessory.""" helper = await setup_test_component(hass, create_leak_sensor_service) - helper.characteristics[LEAK_DETECTED].value = 0 + await helper.async_update( + ServicesTypes.LEAK_SENSOR, {CharacteristicsTypes.LEAK_DETECTED: 0} + ) state = await helper.poll_and_get_state() assert state.state == "off" - helper.characteristics[LEAK_DETECTED].value = 1 + await helper.async_update( + ServicesTypes.LEAK_SENSOR, {CharacteristicsTypes.LEAK_DETECTED: 1} + ) state = await helper.poll_and_get_state() assert state.state == "on" diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py index 020f303ffaa35..79dbc59a38a07 100644 --- a/tests/components/homekit_controller/test_button.py +++ b/tests/components/homekit_controller/test_button.py @@ -9,7 +9,22 @@ def create_switch_with_setup_button(accessory): """Define setup button characteristics.""" service = accessory.add_service(ServicesTypes.OUTLET) - setup = service.add_char(CharacteristicsTypes.Vendor.HAA_SETUP) + setup = service.add_char(CharacteristicsTypes.VENDOR_HAA_SETUP) + + setup.value = "" + setup.format = "string" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + +def create_switch_with_ecobee_clear_hold_button(accessory): + """Define setup button characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + setup = service.add_char(CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD) setup.value = "" setup.format = "string" @@ -25,7 +40,7 @@ async def test_press_button(hass): helper = await setup_test_component(hass, create_switch_with_setup_button) # Helper will be for the primary entity, which is the outlet. Make a helper for the button. - energy_helper = Helper( + button = Helper( hass, "button.testdevice_setup", helper.pairing, @@ -33,13 +48,44 @@ async def test_press_button(hass): helper.config_entry, ) - outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - setup = outlet[CharacteristicsTypes.Vendor.HAA_SETUP] - await hass.services.async_call( "button", "press", {"entity_id": "button.testdevice_setup"}, blocking=True, ) - assert setup.value == "#HAA@trcmd" + button.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_HAA_SETUP: "#HAA@trcmd", + }, + ) + + +async def test_ecobee_clear_hold_press_button(hass): + """Test ecobee clear hold button characteristic is correctly handled.""" + helper = await setup_test_component( + hass, create_switch_with_ecobee_clear_hold_button + ) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the button. + clear_hold = Helper( + hass, + "button.testdevice_clear_hold", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.testdevice_clear_hold"}, + blocking=True, + ) + clear_hold.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_ECOBEE_CLEAR_HOLD: True, + }, + ) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 07a5025ac88ac..9ca45fd53ac19 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -22,21 +22,6 @@ from tests.components.homekit_controller.common import setup_test_component -HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target") -HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current") -THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD = ( - "thermostat", - "temperature.cooling-threshold", -) -THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD = ( - "thermostat", - "temperature.heating-threshold", -) -TEMPERATURE_TARGET = ("thermostat", "temperature.target") -TEMPERATURE_CURRENT = ("thermostat", "temperature.current") -HUMIDITY_TARGET = ("thermostat", "relative-humidity.target") -HUMIDITY_CURRENT = ("thermostat", "relative-humidity.current") - # Test thermostat devices @@ -116,8 +101,12 @@ async def test_climate_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, blocking=True, ) - - assert helper.characteristics[HEATING_COOLING_TARGET].value == 1 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.HEATING_COOLING_TARGET: 1, + }, + ) await hass.services.async_call( DOMAIN, @@ -125,7 +114,12 @@ async def test_climate_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, blocking=True, ) - assert helper.characteristics[HEATING_COOLING_TARGET].value == 2 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.HEATING_COOLING_TARGET: 2, + }, + ) await hass.services.async_call( DOMAIN, @@ -133,7 +127,12 @@ async def test_climate_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, blocking=True, ) - assert helper.characteristics[HEATING_COOLING_TARGET].value == 3 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.HEATING_COOLING_TARGET: 3, + }, + ) await hass.services.async_call( DOMAIN, @@ -141,7 +140,12 @@ async def test_climate_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_OFF}, blocking=True, ) - assert helper.characteristics[HEATING_COOLING_TARGET].value == 0 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.HEATING_COOLING_TARGET: 0, + }, + ) async def test_climate_check_min_max_values_per_mode(hass, utcnow): @@ -189,7 +193,12 @@ async def test_climate_change_thermostat_temperature(hass, utcnow): {"entity_id": "climate.testdevice", "temperature": 21}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + }, + ) await hass.services.async_call( DOMAIN, @@ -197,7 +206,12 @@ async def test_climate_change_thermostat_temperature(hass, utcnow): {"entity_id": "climate.testdevice", "temperature": 25}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 25 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 25, + }, + ) async def test_climate_change_thermostat_temperature_range(hass, utcnow): @@ -222,9 +236,15 @@ async def test_climate_change_thermostat_temperature_range(hass, utcnow): }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 22.5 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 25 + + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 22.5, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 20, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 25, + }, + ) async def test_climate_change_thermostat_temperature_range_iphone(hass, utcnow): @@ -250,9 +270,14 @@ async def test_climate_change_thermostat_temperature_range_iphone(hass, utcnow): }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 22 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 24 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 22, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 20, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 24, + }, + ) async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcnow): @@ -277,9 +302,14 @@ async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcn }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 22 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 0 - assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 0 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 22, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 0, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 0, + }, + ) def create_thermostat_single_set_point_auto(accessory): @@ -359,7 +389,12 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): {"entity_id": "climate.testdevice", "temperature": 21}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + }, + ) await hass.services.async_call( DOMAIN, @@ -367,7 +402,12 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + }, + ) await hass.services.async_call( DOMAIN, @@ -378,7 +418,12 @@ async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 22, + }, + ) async def test_climate_set_mode_via_temp(hass, utcnow): @@ -395,8 +440,13 @@ async def test_climate_set_mode_via_temp(hass, utcnow): }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 21 - assert helper.characteristics[HEATING_COOLING_TARGET].value == 1 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_TARGET: 1, + }, + ) await hass.services.async_call( DOMAIN, @@ -408,8 +458,13 @@ async def test_climate_set_mode_via_temp(hass, utcnow): }, blocking=True, ) - assert helper.characteristics[TEMPERATURE_TARGET].value == 22 - assert helper.characteristics[HEATING_COOLING_TARGET].value == 3 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_TARGET: 22, + CharacteristicsTypes.HEATING_COOLING_TARGET: 3, + }, + ) async def test_climate_change_thermostat_humidity(hass, utcnow): @@ -422,7 +477,12 @@ async def test_climate_change_thermostat_humidity(hass, utcnow): {"entity_id": "climate.testdevice", "humidity": 50}, blocking=True, ) - assert helper.characteristics[HUMIDITY_TARGET].value == 50 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 50, + }, + ) await hass.services.async_call( DOMAIN, @@ -430,7 +490,12 @@ async def test_climate_change_thermostat_humidity(hass, utcnow): {"entity_id": "climate.testdevice", "humidity": 45}, blocking=True, ) - assert helper.characteristics[HUMIDITY_TARGET].value == 45 + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) async def test_climate_read_thermostat_state(hass, utcnow): @@ -438,12 +503,17 @@ async def test_climate_read_thermostat_state(hass, utcnow): helper = await setup_test_component(hass, create_thermostat_service) # Simulate that heating is on - helper.characteristics[TEMPERATURE_CURRENT].value = 19 - helper.characteristics[TEMPERATURE_TARGET].value = 21 - helper.characteristics[HEATING_COOLING_CURRENT].value = 1 - helper.characteristics[HEATING_COOLING_TARGET].value = 1 - helper.characteristics[HUMIDITY_CURRENT].value = 50 - helper.characteristics[HUMIDITY_TARGET].value = 45 + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 19, + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 1, + CharacteristicsTypes.HEATING_COOLING_TARGET: 1, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 50, + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT @@ -453,12 +523,17 @@ async def test_climate_read_thermostat_state(hass, utcnow): assert state.attributes["max_temp"] == 35 # Simulate that cooling is on - helper.characteristics[TEMPERATURE_CURRENT].value = 21 - helper.characteristics[TEMPERATURE_TARGET].value = 19 - helper.characteristics[HEATING_COOLING_CURRENT].value = 2 - helper.characteristics[HEATING_COOLING_TARGET].value = 2 - helper.characteristics[HUMIDITY_CURRENT].value = 45 - helper.characteristics[HUMIDITY_TARGET].value = 45 + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 21, + CharacteristicsTypes.TEMPERATURE_TARGET: 19, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 2, + CharacteristicsTypes.HEATING_COOLING_TARGET: 2, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 45, + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_COOL @@ -466,10 +541,15 @@ async def test_climate_read_thermostat_state(hass, utcnow): assert state.attributes["current_humidity"] == 45 # Simulate that we are in heat/cool mode - helper.characteristics[TEMPERATURE_CURRENT].value = 21 - helper.characteristics[TEMPERATURE_TARGET].value = 21 - helper.characteristics[HEATING_COOLING_CURRENT].value = 0 - helper.characteristics[HEATING_COOLING_TARGET].value = 3 + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 21, + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 0, + CharacteristicsTypes.HEATING_COOLING_TARGET: 3, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT_COOL @@ -481,12 +561,17 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): # Simulate that current temperature is above target temp # Heating might be on, but hvac_action currently 'off' - helper.characteristics[TEMPERATURE_CURRENT].value = 22 - helper.characteristics[TEMPERATURE_TARGET].value = 21 - helper.characteristics[HEATING_COOLING_CURRENT].value = 0 - helper.characteristics[HEATING_COOLING_TARGET].value = 1 - helper.characteristics[HUMIDITY_CURRENT].value = 50 - helper.characteristics[HUMIDITY_TARGET].value = 45 + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 22, + CharacteristicsTypes.TEMPERATURE_TARGET: 21, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 0, + CharacteristicsTypes.HEATING_COOLING_TARGET: 1, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 50, + CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET: 45, + }, + ) state = await helper.poll_and_get_state() assert state.state == "heat" @@ -494,23 +579,19 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): # Simulate that current temperature is below target temp # Heating might be on and hvac_action currently 'heat' - helper.characteristics[TEMPERATURE_CURRENT].value = 19 - helper.characteristics[HEATING_COOLING_CURRENT].value = 1 + await helper.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 19, + CharacteristicsTypes.HEATING_COOLING_CURRENT: 1, + }, + ) state = await helper.poll_and_get_state() assert state.state == "heat" assert state.attributes["hvac_action"] == "heating" -TARGET_HEATER_COOLER_STATE = ("heater-cooler", "heater-cooler.state.target") -CURRENT_HEATER_COOLER_STATE = ("heater-cooler", "heater-cooler.state.current") -HEATER_COOLER_ACTIVE = ("heater-cooler", "active") -HEATER_COOLER_TEMPERATURE_CURRENT = ("heater-cooler", "temperature.current") -TEMPERATURE_COOLING_THRESHOLD = ("heater-cooler", "temperature.cooling-threshold") -TEMPERATURE_HEATING_THRESHOLD = ("heater-cooler", "temperature.heating-threshold") -SWING_MODE = ("heater-cooler", "swing-mode") - - def create_heater_cooler_service(accessory): """Define thermostat characteristics.""" service = accessory.add_service(ServicesTypes.HEATER_COOLER) @@ -583,10 +664,11 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, blocking=True, ) - - assert ( - helper.characteristics[TARGET_HEATER_COOLER_STATE].value - == TargetHeaterCoolerStateValues.HEAT + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + }, ) await hass.services.async_call( @@ -595,9 +677,11 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, blocking=True, ) - assert ( - helper.characteristics[TARGET_HEATER_COOLER_STATE].value - == TargetHeaterCoolerStateValues.COOL + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.COOL, + }, ) await hass.services.async_call( @@ -606,9 +690,11 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, blocking=True, ) - assert ( - helper.characteristics[TARGET_HEATER_COOLER_STATE].value - == TargetHeaterCoolerStateValues.AUTOMATIC + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.AUTOMATIC, + }, ) await hass.services.async_call( @@ -617,9 +703,11 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_OFF}, blocking=True, ) - assert ( - helper.characteristics[HEATER_COOLER_ACTIVE].value - == ActivationStateValues.INACTIVE + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE, + }, ) @@ -639,7 +727,12 @@ async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): {"entity_id": "climate.testdevice", "temperature": 20}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value == 20 + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 20, + }, + ) await hass.services.async_call( DOMAIN, @@ -653,7 +746,12 @@ async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): {"entity_id": "climate.testdevice", "temperature": 26}, blocking=True, ) - assert helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value == 26 + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 26, + }, + ) async def test_heater_cooler_read_thermostat_state(hass, utcnow): @@ -661,15 +759,16 @@ async def test_heater_cooler_read_thermostat_state(hass, utcnow): helper = await setup_test_component(hass, create_heater_cooler_service) # Simulate that heating is on - helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 19 - helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value = 20 - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.HEATING - helper.characteristics[ - TARGET_HEATER_COOLER_STATE - ].value = TargetHeaterCoolerStateValues.HEAT - helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 19, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 21, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.HEATING, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT @@ -678,30 +777,32 @@ async def test_heater_cooler_read_thermostat_state(hass, utcnow): assert state.attributes["max_temp"] == 35 # Simulate that cooling is on - helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 21 - helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value = 19 - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.COOLING - helper.characteristics[ - TARGET_HEATER_COOLER_STATE - ].value = TargetHeaterCoolerStateValues.COOL - helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 21, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 19, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.COOLING, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.COOL, + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_COOL assert state.attributes["current_temperature"] == 21 # Simulate that we are in auto mode - helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 21 - helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value = 21 - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.COOLING - helper.characteristics[ - TARGET_HEATER_COOLER_STATE - ].value = TargetHeaterCoolerStateValues.AUTOMATIC - helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 21, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: 21, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.COOLING, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.AUTOMATIC, + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT_COOL @@ -713,15 +814,16 @@ async def test_heater_cooler_hvac_mode_vs_hvac_action(hass, utcnow): # Simulate that current temperature is above target temp # Heating might be on, but hvac_action currently 'off' - helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 22 - helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value = 21 - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.IDLE - helper.characteristics[ - TARGET_HEATER_COOLER_STATE - ].value = TargetHeaterCoolerStateValues.HEAT - helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 22, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 21, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.IDLE, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) state = await helper.poll_and_get_state() assert state.state == "heat" @@ -729,10 +831,16 @@ async def test_heater_cooler_hvac_mode_vs_hvac_action(hass, utcnow): # Simulate that current temperature is below target temp # Heating might be on and hvac_action currently 'heat' - helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 19 - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.HEATING + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 19, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: 21, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.HEATING, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) state = await helper.poll_and_get_state() assert state.state == "heat" @@ -749,7 +857,12 @@ async def test_heater_cooler_change_swing_mode(hass, utcnow): {"entity_id": "climate.testdevice", "swing_mode": "vertical"}, blocking=True, ) - assert helper.characteristics[SWING_MODE].value == SwingModeValues.ENABLED + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.SWING_MODE: SwingModeValues.ENABLED, + }, + ) await hass.services.async_call( DOMAIN, @@ -757,20 +870,28 @@ async def test_heater_cooler_change_swing_mode(hass, utcnow): {"entity_id": "climate.testdevice", "swing_mode": "off"}, blocking=True, ) - assert helper.characteristics[SWING_MODE].value == SwingModeValues.DISABLED + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.SWING_MODE: SwingModeValues.DISABLED, + }, + ) async def test_heater_cooler_turn_off(hass, utcnow): """Test that both hvac_action and hvac_mode return "off" when turned off.""" helper = await setup_test_component(hass, create_heater_cooler_service) + # Simulate that the device is turned off but CURRENT_HEATER_COOLER_STATE still returns HEATING/COOLING - helper.characteristics[HEATER_COOLER_ACTIVE].value = ActivationStateValues.INACTIVE - helper.characteristics[ - CURRENT_HEATER_COOLER_STATE - ].value = CurrentHeaterCoolerStateValues.HEATING - helper.characteristics[ - TARGET_HEATER_COOLER_STATE - ].value = TargetHeaterCoolerStateValues.HEAT + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE: CurrentHeaterCoolerStateValues.HEATING, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + }, + ) + state = await helper.poll_and_get_state() assert state.state == "off" assert state.attributes["hvac_action"] == "off" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 33b5b15698d40..a65d63b1af2c6 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -85,14 +85,12 @@ def _setup_flow_handler(hass, pairing=None): finish_pairing = unittest.mock.AsyncMock(return_value=pairing) discovery = mock.Mock() - discovery.device_id = "00:00:00:00:00:00" - discovery.start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) + discovery.description.id = "00:00:00:00:00:00" + discovery.async_start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) flow.controller = mock.Mock() flow.controller.pairings = {} - flow.controller.find_ip_by_device_id = unittest.mock.AsyncMock( - return_value=discovery - ) + flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery) return flow @@ -138,21 +136,21 @@ def get_device_discovery_info( device, upper_case_props=False, missing_csharp=False ) -> zeroconf.ZeroconfServiceInfo: """Turn a aiohomekit format zeroconf entry into a homeassistant one.""" - record = device.info result = zeroconf.ZeroconfServiceInfo( - host=record["address"], - hostname=record["name"], - name=record["name"], - port=record["port"], + host="127.0.0.1", + hostname=device.description.name, + name=device.description.name, + addresses=["127.0.0.1"], + port=8080, properties={ - "md": record["md"], - "pv": record["pv"], - zeroconf.ATTR_PROPERTIES_ID: device.device_id, - "c#": record["c#"], - "s#": record["s#"], - "ff": record["ff"], - "ci": record["ci"], - "sf": 0x01, # record["sf"], + "md": device.description.model, + "pv": "1.0", + zeroconf.ATTR_PROPERTIES_ID: device.description.id, + "c#": device.description.config_num, + "s#": device.description.state_num, + "ff": "0", + "ci": "0", + "sf": "1", "sh": "", }, type="_hap._tcp.local.", @@ -180,6 +178,7 @@ def setup_mock_accessory(controller): serial_number="12345", firmware_revision="1.1", ) + accessory.aid = 1 service = accessory.add_service(ServicesTypes.LIGHTBULB) on_char = service.add_char(CharacteristicsTypes.ON) @@ -518,7 +517,7 @@ async def test_pair_abort_errors_on_start(hass, controller, exception, expected) # User initiates pairing - device refuses to enter pairing mode test_exc = exception("error") - with patch.object(device, "start_pairing", side_effect=test_exc): + with patch.object(device, "async_start_pairing", side_effect=test_exc): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == expected @@ -540,7 +539,7 @@ async def test_pair_try_later_errors_on_start(hass, controller, exception, expec # User initiates pairing - device refuses to enter pairing mode but may be successful after entering pairing mode or rebooting test_exc = exception("error") - with patch.object(device, "start_pairing", side_effect=test_exc): + with patch.object(device, "async_start_pairing", side_effect=test_exc): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["step_id"] == expected assert result2["type"] == "form" @@ -583,7 +582,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): # User initiates pairing - device refuses to enter pairing mode test_exc = exception("error") - with patch.object(device, "start_pairing", side_effect=test_exc): + with patch.object(device, "async_start_pairing", side_effect=test_exc): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"pairing_code": "111-22-333"} ) @@ -632,7 +631,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) - with patch.object(device, "start_pairing", return_value=finish_pairing): + 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" @@ -672,7 +671,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) - with patch.object(device, "start_pairing", return_value=finish_pairing): + 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" @@ -787,7 +786,7 @@ async def test_user_no_unpaired_devices(hass, controller): device = setup_mock_accessory(controller) # Pair the mock device so that it shows as paired in discovery - finish_pairing = await device.start_pairing(device.device_id) + finish_pairing = await device.async_start_pairing(device.description.id) await finish_pairing(device.pairing_code) # Device discovery is requested @@ -807,7 +806,7 @@ async def test_unignore_works(hass, controller): result = await hass.config_entries.flow.async_init( "homekit_controller", context={"source": config_entries.SOURCE_UNIGNORE}, - data={"unique_id": device.device_id}, + data={"unique_id": device.description.id}, ) assert result["type"] == "form" assert result["step_id"] == "pair" @@ -842,7 +841,7 @@ async def test_unignore_ignores_missing_devices(hass, controller): ) assert result["type"] == "abort" - assert result["reason"] == "no_devices" + assert result["reason"] == "accessory_not_found_error" async def test_discovery_dismiss_existing_flow_on_paired(hass, controller): diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 45514b291222d..35e6933f2cd85 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -4,23 +4,6 @@ from tests.components.homekit_controller.common import setup_test_component -POSITION_STATE = ("window-covering", "position.state") -POSITION_CURRENT = ("window-covering", "position.current") -POSITION_TARGET = ("window-covering", "position.target") -POSITION_HOLD = ("window-covering", "position.hold") - -H_TILT_CURRENT = ("window-covering", "horizontal-tilt.current") -H_TILT_TARGET = ("window-covering", "horizontal-tilt.target") - -V_TILT_CURRENT = ("window-covering", "vertical-tilt.current") -V_TILT_TARGET = ("window-covering", "vertical-tilt.target") - -WINDOW_OBSTRUCTION = ("window-covering", "obstruction-detected") - -DOOR_CURRENT = ("garage-door-opener", "door-state.current") -DOOR_TARGET = ("garage-door-opener", "door-state.target") -DOOR_OBSTRUCTION = ("garage-door-opener", "obstruction-detected") - def create_window_covering_service(accessory): """Define a window-covering characteristics as per page 219 of HAP spec.""" @@ -76,31 +59,53 @@ async def test_change_window_cover_state(hass, utcnow): await hass.services.async_call( "cover", "open_cover", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[POSITION_TARGET].value == 100 + helper.async_assert_service_values( + ServicesTypes.WINDOW_COVERING, + { + CharacteristicsTypes.POSITION_TARGET: 100, + }, + ) await hass.services.async_call( "cover", "close_cover", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[POSITION_TARGET].value == 0 + helper.async_assert_service_values( + ServicesTypes.WINDOW_COVERING, + { + CharacteristicsTypes.POSITION_TARGET: 0, + }, + ) async def test_read_window_cover_state(hass, utcnow): """Test that we can read the state of a HomeKit alarm accessory.""" helper = await setup_test_component(hass, create_window_covering_service) - helper.characteristics[POSITION_STATE].value = 0 + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.POSITION_STATE: 0}, + ) state = await helper.poll_and_get_state() assert state.state == "closing" - helper.characteristics[POSITION_STATE].value = 1 + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.POSITION_STATE: 1}, + ) state = await helper.poll_and_get_state() assert state.state == "opening" - helper.characteristics[POSITION_STATE].value = 2 + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.POSITION_STATE: 2}, + ) state = await helper.poll_and_get_state() assert state.state == "closed" - helper.characteristics[WINDOW_OBSTRUCTION].value = True + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.OBSTRUCTION_DETECTED: True}, + ) state = await helper.poll_and_get_state() assert state.attributes["obstruction-detected"] is True @@ -111,7 +116,10 @@ async def test_read_window_cover_tilt_horizontal(hass, utcnow): hass, create_window_covering_service_with_h_tilt ) - helper.characteristics[H_TILT_CURRENT].value = 75 + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.HORIZONTAL_TILT_CURRENT: 75}, + ) state = await helper.poll_and_get_state() assert state.attributes["current_tilt_position"] == 75 @@ -122,7 +130,10 @@ async def test_read_window_cover_tilt_vertical(hass, utcnow): hass, create_window_covering_service_with_v_tilt ) - helper.characteristics[V_TILT_CURRENT].value = 75 + await helper.async_update( + ServicesTypes.WINDOW_COVERING, + {CharacteristicsTypes.VERTICAL_TILT_CURRENT: 75}, + ) state = await helper.poll_and_get_state() assert state.attributes["current_tilt_position"] == 75 @@ -139,7 +150,12 @@ async def test_write_window_cover_tilt_horizontal(hass, utcnow): {"entity_id": helper.entity_id, "tilt_position": 90}, blocking=True, ) - assert helper.characteristics[H_TILT_TARGET].value == 90 + helper.async_assert_service_values( + ServicesTypes.WINDOW_COVERING, + { + CharacteristicsTypes.HORIZONTAL_TILT_TARGET: 90, + }, + ) async def test_write_window_cover_tilt_vertical(hass, utcnow): @@ -154,7 +170,12 @@ async def test_write_window_cover_tilt_vertical(hass, utcnow): {"entity_id": helper.entity_id, "tilt_position": 90}, blocking=True, ) - assert helper.characteristics[V_TILT_TARGET].value == 90 + helper.async_assert_service_values( + ServicesTypes.WINDOW_COVERING, + { + CharacteristicsTypes.VERTICAL_TILT_TARGET: 90, + }, + ) async def test_window_cover_stop(hass, utcnow): @@ -166,7 +187,12 @@ async def test_window_cover_stop(hass, utcnow): await hass.services.async_call( "cover", "stop_cover", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[POSITION_HOLD].value == 1 + helper.async_assert_service_values( + ServicesTypes.WINDOW_COVERING, + { + CharacteristicsTypes.POSITION_HOLD: True, + }, + ) def create_garage_door_opener_service(accessory): @@ -195,34 +221,59 @@ async def test_change_door_state(hass, utcnow): await hass.services.async_call( "cover", "open_cover", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[DOOR_TARGET].value == 0 + helper.async_assert_service_values( + ServicesTypes.GARAGE_DOOR_OPENER, + { + CharacteristicsTypes.DOOR_STATE_TARGET: 0, + }, + ) await hass.services.async_call( "cover", "close_cover", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[DOOR_TARGET].value == 1 + helper.async_assert_service_values( + ServicesTypes.GARAGE_DOOR_OPENER, + { + CharacteristicsTypes.DOOR_STATE_TARGET: 1, + }, + ) async def test_read_door_state(hass, utcnow): """Test that we can read the state of a HomeKit garage door.""" helper = await setup_test_component(hass, create_garage_door_opener_service) - helper.characteristics[DOOR_CURRENT].value = 0 + await helper.async_update( + ServicesTypes.GARAGE_DOOR_OPENER, + {CharacteristicsTypes.DOOR_STATE_CURRENT: 0}, + ) state = await helper.poll_and_get_state() assert state.state == "open" - helper.characteristics[DOOR_CURRENT].value = 1 + await helper.async_update( + ServicesTypes.GARAGE_DOOR_OPENER, + {CharacteristicsTypes.DOOR_STATE_CURRENT: 1}, + ) state = await helper.poll_and_get_state() assert state.state == "closed" - helper.characteristics[DOOR_CURRENT].value = 2 + await helper.async_update( + ServicesTypes.GARAGE_DOOR_OPENER, + {CharacteristicsTypes.DOOR_STATE_CURRENT: 2}, + ) state = await helper.poll_and_get_state() assert state.state == "opening" - helper.characteristics[DOOR_CURRENT].value = 3 + await helper.async_update( + ServicesTypes.GARAGE_DOOR_OPENER, + {CharacteristicsTypes.DOOR_STATE_CURRENT: 3}, + ) state = await helper.poll_and_get_state() assert state.state == "closing" - helper.characteristics[DOOR_OBSTRUCTION].value = True + await helper.async_update( + ServicesTypes.GARAGE_DOOR_OPENER, + {CharacteristicsTypes.OBSTRUCTION_DETECTED: True}, + ) state = await helper.poll_and_get_state() assert state.attributes["obstruction-detected"] is True diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index bd9aa30f6aebe..8d22f7ff4bc1a 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -151,7 +151,7 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc }, { "iid": 13, - "type": "4aaaf940-0dec-11e5-b939-0800200c9a66", + "type": "4AAAF940-0DEC-11E5-B939-0800200C9A66", "characteristics": [ { "type": "4AAAF942-0DEC-11E5-B939-0800200C9A66", @@ -422,7 +422,7 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): }, { "iid": 13, - "type": "4aaaf940-0dec-11e5-b939-0800200c9a66", + "type": "4AAAF940-0DEC-11E5-B939-0800200C9A66", "characteristics": [ { "type": "4AAAF942-0DEC-11E5-B939-0800200C9A66", diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index d66ce81d5349f..252e7f87bed7e 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -4,15 +4,6 @@ from tests.components.homekit_controller.common import setup_test_component -V1_ON = ("fan", "on") -V1_ROTATION_DIRECTION = ("fan", "rotation.direction") -V1_ROTATION_SPEED = ("fan", "rotation.speed") - -V2_ACTIVE = ("fanv2", "active") -V2_ROTATION_DIRECTION = ("fanv2", "rotation.direction") -V2_ROTATION_SPEED = ("fanv2", "rotation.speed") -V2_SWING_MODE = ("fanv2", "swing-mode") - def create_fan_service(accessory): """ @@ -86,12 +77,14 @@ async def test_fan_read_state(hass, utcnow): """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ON].value = False - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, {CharacteristicsTypes.ON: False} + ) assert state.state == "off" - helper.characteristics[V1_ON].value = True - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, {CharacteristicsTypes.ON: True} + ) assert state.state == "on" @@ -105,8 +98,13 @@ async def test_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "high"}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 1 - assert helper.characteristics[V1_ROTATION_SPEED].value == 100 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 1, + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) await hass.services.async_call( "fan", @@ -114,8 +112,13 @@ async def test_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 1 - assert helper.characteristics[V1_ROTATION_SPEED].value == 66.0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 1, + CharacteristicsTypes.ROTATION_SPEED: 66.0, + }, + ) await hass.services.async_call( "fan", @@ -123,8 +126,13 @@ async def test_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 1 - assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 1, + CharacteristicsTypes.ROTATION_SPEED: 33.0, + }, + ) async def test_turn_on_off_without_rotation_speed(hass, utcnow): @@ -139,7 +147,12 @@ async def test_turn_on_off_without_rotation_speed(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + }, + ) await hass.services.async_call( "fan", @@ -147,14 +160,19 @@ async def test_turn_on_off_without_rotation_speed(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_turn_off(hass, utcnow): """Test that we can turn a fan off.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ON].value = 1 + await helper.async_update(ServicesTypes.FAN, {CharacteristicsTypes.ON: 1}) await hass.services.async_call( "fan", @@ -162,14 +180,19 @@ async def test_turn_off(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 0, + }, + ) async def test_set_speed(hass, utcnow): """Test that we set fan speed.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ON].value = 1 + await helper.async_update(ServicesTypes.FAN, {CharacteristicsTypes.ON: 1}) await hass.services.async_call( "fan", @@ -177,7 +200,12 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "high"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 100 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 100.0, + }, + ) await hass.services.async_call( "fan", @@ -185,7 +213,12 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 66.0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 66.0, + }, + ) await hass.services.async_call( "fan", @@ -193,7 +226,12 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 33.0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 33.0, + }, + ) await hass.services.async_call( "fan", @@ -201,14 +239,19 @@ async def test_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "off"}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 0, + }, + ) async def test_set_percentage(hass, utcnow): """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ON].value = 1 + await helper.async_update(ServicesTypes.FAN, {CharacteristicsTypes.ON: 1}) await hass.services.async_call( "fan", @@ -216,7 +259,12 @@ async def test_set_percentage(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_SPEED].value == 66 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) await hass.services.async_call( "fan", @@ -224,33 +272,54 @@ async def test_set_percentage(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) - assert helper.characteristics[V1_ON].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 0, + }, + ) async def test_speed_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ON].value = 1 - helper.characteristics[V1_ROTATION_SPEED].value = 100 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 1, + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 assert state.attributes["percentage_step"] == 1.0 - helper.characteristics[V1_ROTATION_SPEED].value = 50 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 50, + }, + ) assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 - helper.characteristics[V1_ROTATION_SPEED].value = 25 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_SPEED: 25, + }, + ) assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 - helper.characteristics[V1_ON].value = 0 - helper.characteristics[V1_ROTATION_SPEED].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, + { + CharacteristicsTypes.ON: 0, + CharacteristicsTypes.ROTATION_SPEED: 0, + }, + ) assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 @@ -265,7 +334,12 @@ async def test_set_direction(hass, utcnow): {"entity_id": "fan.testdevice", "direction": "reverse"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_DIRECTION].value == 1 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_DIRECTION: 1, + }, + ) await hass.services.async_call( "fan", @@ -273,19 +347,26 @@ async def test_set_direction(hass, utcnow): {"entity_id": "fan.testdevice", "direction": "forward"}, blocking=True, ) - assert helper.characteristics[V1_ROTATION_DIRECTION].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN, + { + CharacteristicsTypes.ROTATION_DIRECTION: 0, + }, + ) async def test_direction_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fan_service) - helper.characteristics[V1_ROTATION_DIRECTION].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, {CharacteristicsTypes.ROTATION_DIRECTION: 0} + ) assert state.attributes["direction"] == "forward" - helper.characteristics[V1_ROTATION_DIRECTION].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN, {CharacteristicsTypes.ROTATION_DIRECTION: 1} + ) assert state.attributes["direction"] == "reverse" @@ -293,12 +374,14 @@ async def test_fanv2_read_state(hass, utcnow): """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ACTIVE].value = False - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: False} + ) assert state.state == "off" - helper.characteristics[V2_ACTIVE].value = True - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: True} + ) assert state.state == "on" @@ -312,8 +395,13 @@ async def test_v2_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "high"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 100 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) await hass.services.async_call( "fan", @@ -321,8 +409,13 @@ async def test_v2_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 66.0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) await hass.services.async_call( "fan", @@ -330,8 +423,13 @@ async def test_v2_turn_on(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) await hass.services.async_call( "fan", @@ -339,8 +437,13 @@ async def test_v2_turn_on(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 - assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) await hass.services.async_call( "fan", @@ -348,15 +451,20 @@ async def test_v2_turn_on(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 1 - assert helper.characteristics[V2_ROTATION_SPEED].value == 33.0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) async def test_v2_turn_off(hass, utcnow): """Test that we can turn a fan off.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ACTIVE].value = 1 + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) await hass.services.async_call( "fan", @@ -364,14 +472,19 @@ async def test_v2_turn_off(hass, utcnow): {"entity_id": "fan.testdevice"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_v2_set_speed(hass, utcnow): """Test that we set fan speed.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ACTIVE].value = 1 + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) await hass.services.async_call( "fan", @@ -379,7 +492,12 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "high"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 100 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) await hass.services.async_call( "fan", @@ -387,7 +505,12 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "medium"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 66 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) await hass.services.async_call( "fan", @@ -395,7 +518,12 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "low"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 33 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) await hass.services.async_call( "fan", @@ -403,14 +531,19 @@ async def test_v2_set_speed(hass, utcnow): {"entity_id": "fan.testdevice", "speed": "off"}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_v2_set_percentage(hass, utcnow): """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ACTIVE].value = 1 + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) await hass.services.async_call( "fan", @@ -418,7 +551,12 @@ async def test_v2_set_percentage(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 66 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) await hass.services.async_call( "fan", @@ -426,14 +564,19 @@ async def test_v2_set_percentage(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_v2_set_percentage_with_min_step(hass, utcnow): """Test that we set fan speed by percentage.""" helper = await setup_test_component(hass, create_fanv2_service_with_min_step) - helper.characteristics[V2_ACTIVE].value = 1 + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) await hass.services.async_call( "fan", @@ -441,7 +584,12 @@ async def test_v2_set_percentage_with_min_step(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_SPEED].value == 75 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 75, + }, + ) await hass.services.async_call( "fan", @@ -449,32 +597,53 @@ async def test_v2_set_percentage_with_min_step(hass, utcnow): {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) - assert helper.characteristics[V2_ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_v2_speed_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ACTIVE].value = 1 - helper.characteristics[V2_ROTATION_SPEED].value = 100 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 - helper.characteristics[V2_ROTATION_SPEED].value = 50 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 50, + }, + ) assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 - helper.characteristics[V2_ROTATION_SPEED].value = 25 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 25, + }, + ) assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 - helper.characteristics[V2_ACTIVE].value = 0 - helper.characteristics[V2_ROTATION_SPEED].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + CharacteristicsTypes.ROTATION_SPEED: 0, + }, + ) assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 @@ -489,7 +658,12 @@ async def test_v2_set_direction(hass, utcnow): {"entity_id": "fan.testdevice", "direction": "reverse"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_DIRECTION].value == 1 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_DIRECTION: 1, + }, + ) await hass.services.async_call( "fan", @@ -497,19 +671,26 @@ async def test_v2_set_direction(hass, utcnow): {"entity_id": "fan.testdevice", "direction": "forward"}, blocking=True, ) - assert helper.characteristics[V2_ROTATION_DIRECTION].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_DIRECTION: 0, + }, + ) async def test_v2_direction_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_ROTATION_DIRECTION].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.ROTATION_DIRECTION: 0} + ) assert state.attributes["direction"] == "forward" - helper.characteristics[V2_ROTATION_DIRECTION].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.ROTATION_DIRECTION: 1} + ) assert state.attributes["direction"] == "reverse" @@ -523,7 +704,12 @@ async def test_v2_oscillate(hass, utcnow): {"entity_id": "fan.testdevice", "oscillating": True}, blocking=True, ) - assert helper.characteristics[V2_SWING_MODE].value == 1 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.SWING_MODE: 1, + }, + ) await hass.services.async_call( "fan", @@ -531,17 +717,24 @@ async def test_v2_oscillate(hass, utcnow): {"entity_id": "fan.testdevice", "oscillating": False}, blocking=True, ) - assert helper.characteristics[V2_SWING_MODE].value == 0 + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.SWING_MODE: 0, + }, + ) async def test_v2_oscillate_read(hass, utcnow): """Test that we can read a fans oscillation.""" helper = await setup_test_component(hass, create_fanv2_service) - helper.characteristics[V2_SWING_MODE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 0} + ) assert state.attributes["oscillating"] is False - helper.characteristics[V2_SWING_MODE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1} + ) assert state.attributes["oscillating"] is True diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index 0af795e2ce941..ea32b1931c2c9 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -7,25 +7,6 @@ from tests.components.homekit_controller.common import setup_test_component -ACTIVE = ("humidifier-dehumidifier", "active") -CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE = ( - "humidifier-dehumidifier", - "humidifier-dehumidifier.state.current", -) -TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE = ( - "humidifier-dehumidifier", - "humidifier-dehumidifier.state.target", -) -RELATIVE_HUMIDITY_CURRENT = ("humidifier-dehumidifier", "relative-humidity.current") -RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD = ( - "humidifier-dehumidifier", - "relative-humidity.humidifier-threshold", -) -RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD = ( - "humidifier-dehumidifier", - "relative-humidity.dehumidifier-threshold", -) - def create_humidifier_service(accessory): """Define a humidifier characteristics as per page 219 of HAP spec.""" @@ -89,13 +70,19 @@ async def test_humidifier_active_state(hass, utcnow): DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.ACTIVE: 1}, + ) await hass.services.async_call( DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.ACTIVE: 0}, + ) async def test_dehumidifier_active_state(hass, utcnow): @@ -106,54 +93,85 @@ async def test_dehumidifier_active_state(hass, utcnow): DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.ACTIVE: 1}, + ) await hass.services.async_call( DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True ) - assert helper.characteristics[ACTIVE].value == 0 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.ACTIVE: 0}, + ) async def test_humidifier_read_humidity(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) - helper.characteristics[ACTIVE].value = True - helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 75 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: True, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: 75, + }, + ) assert state.state == "on" assert state.attributes["humidity"] == 75 - helper.characteristics[ACTIVE].value = False - helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 10 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: False, + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: 10, + }, + ) assert state.state == "off" assert state.attributes["humidity"] == 10 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 3, + }, + ) assert state.attributes["humidity"] == 10 + assert state.state == "off" async def test_dehumidifier_read_humidity(hass, utcnow): """Test that we can read the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) - helper.characteristics[ACTIVE].value = True - helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 75 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: True, + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: 75, + }, + ) assert state.state == "on" assert state.attributes["humidity"] == 75 - helper.characteristics[ACTIVE].value = False - helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: False, + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: 40, + }, + ) assert state.state == "off" assert state.attributes["humidity"] == 40 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) assert state.attributes["humidity"] == 40 @@ -167,7 +185,10 @@ async def test_humidifier_set_humidity(hass, utcnow): {"entity_id": helper.entity_id, "humidity": 20}, blocking=True, ) - assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: 20}, + ) async def test_dehumidifier_set_humidity(hass, utcnow): @@ -180,7 +201,10 @@ async def test_dehumidifier_set_humidity(hass, utcnow): {"entity_id": helper.entity_id, "humidity": 20}, blocking=True, ) - assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 20 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + {CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: 20}, + ) async def test_humidifier_set_mode(hass, utcnow): @@ -193,8 +217,13 @@ async def test_humidifier_set_mode(hass, utcnow): {"entity_id": helper.entity_id, "mode": MODE_AUTO}, blocking=True, ) - assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) await hass.services.async_call( DOMAIN, @@ -202,8 +231,13 @@ async def test_humidifier_set_mode(hass, utcnow): {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, blocking=True, ) - assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 1 - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + }, + ) async def test_dehumidifier_set_mode(hass, utcnow): @@ -216,8 +250,13 @@ async def test_dehumidifier_set_mode(hass, utcnow): {"entity_id": helper.entity_id, "mode": MODE_AUTO}, blocking=True, ) - assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0 - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) await hass.services.async_call( DOMAIN, @@ -225,8 +264,13 @@ async def test_dehumidifier_set_mode(hass, utcnow): {"entity_id": helper.entity_id, "mode": MODE_NORMAL}, blocking=True, ) - assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2 - assert helper.characteristics[ACTIVE].value == 1 + helper.async_assert_service_values( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.ACTIVE: 1, + CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) async def test_humidifier_read_only_mode(hass, utcnow): @@ -236,20 +280,36 @@ async def test_humidifier_read_only_mode(hass, utcnow): state = await helper.poll_and_get_state() assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + }, + ) assert state.attributes["mode"] == "auto" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 3, + }, + ) assert state.attributes["mode"] == "normal" @@ -260,20 +320,36 @@ async def test_dehumidifier_read_only_mode(hass, utcnow): state = await helper.poll_and_get_state() assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + }, + ) assert state.attributes["mode"] == "auto" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) assert state.attributes["mode"] == "normal" - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 3, + }, + ) assert state.attributes["mode"] == "normal" @@ -281,26 +357,41 @@ async def test_humidifier_target_humidity_modes(hass, utcnow): """Test that we can read the state of a HomeKit humidifier accessory.""" helper = await setup_test_component(hass, create_humidifier_service) - helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 37 - helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 - - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: 37, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 51, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + }, + ) assert state.attributes["mode"] == "auto" assert state.attributes["humidity"] == 37 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 3, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 37 @@ -309,25 +400,40 @@ async def test_dehumidifier_target_humidity_modes(hass, utcnow): """Test that we can read the state of a HomeKit dehumidifier accessory.""" helper = await setup_test_component(hass, create_dehumidifier_service) - helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73 - helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1 - - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: 73, + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 51, + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 1, + }, + ) assert state.attributes["mode"] == "auto" assert state.attributes["humidity"] == 73 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 3, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 73 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 2, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 73 - helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDIFIER_DEHUMIDIFIER, + { + CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE: 0, + }, + ) assert state.attributes["mode"] == "normal" assert state.attributes["humidity"] == 73 diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index d1b133468d5a9..03694e7186ab1 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -4,7 +4,6 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from aiohomekit.testing import FakeController from homeassistant.components.homekit_controller.const import ENTITY_MAP from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -35,19 +34,16 @@ async def test_unload_on_stop(hass, utcnow): async def test_async_remove_entry(hass: HomeAssistant): """Test unpairing a component.""" helper = await setup_test_component(hass, create_motion_sensor_service) + controller = helper.pairing.controller hkid = "00:00:00:00:00:00" - with patch("aiohomekit.Controller") as controller_cls: - # Setup a fake controller with 1 pairing - controller = controller_cls.return_value = FakeController() - await controller.add_paired_device([helper.accessory], hkid) - assert len(controller.pairings) == 1 + assert len(controller.pairings) == 1 - assert hkid in hass.data[ENTITY_MAP].storage_data + assert hkid in hass.data[ENTITY_MAP].storage_data - # Remove it via config entry and number of pairings should go down - await helper.config_entry.async_remove(hass) - assert len(controller.pairings) == 0 + # Remove it via config entry and number of pairings should go down + await helper.config_entry.async_remove(hass) + assert len(controller.pairings) == 0 - assert hkid not in hass.data[ENTITY_MAP].storage_data + assert hkid not in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index f495051206333..8d094c4eb7095 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -10,12 +10,6 @@ LIGHT_BULB_NAME = "Light Bulb" LIGHT_BULB_ENTITY_ID = "light.testdevice" -LIGHT_ON = ("lightbulb", "on") -LIGHT_BRIGHTNESS = ("lightbulb", "brightness") -LIGHT_HUE = ("lightbulb", "hue") -LIGHT_SATURATION = ("lightbulb", "saturation") -LIGHT_COLOR_TEMP = ("lightbulb", "color-temperature") - def create_lightbulb_service(accessory): """Define lightbulb characteristics.""" @@ -63,16 +57,25 @@ async def test_switch_change_light_state(hass, utcnow): {"entity_id": "light.testdevice", "brightness": 255, "hs_color": [4, 5]}, blocking=True, ) - - assert helper.characteristics[LIGHT_ON].value == 1 - assert helper.characteristics[LIGHT_BRIGHTNESS].value == 100 - assert helper.characteristics[LIGHT_HUE].value == 4 - assert helper.characteristics[LIGHT_SATURATION].value == 5 + helper.async_assert_service_values( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.HUE: 4, + CharacteristicsTypes.SATURATION: 5, + }, + ) await hass.services.async_call( "light", "turn_off", {"entity_id": "light.testdevice"}, blocking=True ) - assert helper.characteristics[LIGHT_ON].value == 0 + helper.async_assert_service_values( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: False, + }, + ) async def test_switch_change_light_state_color_temp(hass, utcnow): @@ -85,9 +88,14 @@ async def test_switch_change_light_state_color_temp(hass, utcnow): {"entity_id": "light.testdevice", "brightness": 255, "color_temp": 400}, blocking=True, ) - assert helper.characteristics[LIGHT_ON].value == 1 - assert helper.characteristics[LIGHT_BRIGHTNESS].value == 100 - assert helper.characteristics[LIGHT_COLOR_TEMP].value == 400 + helper.async_assert_service_values( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.COLOR_TEMPERATURE: 400, + }, + ) async def test_switch_read_light_state(hass, utcnow): @@ -99,18 +107,26 @@ async def test_switch_read_light_state(hass, utcnow): assert state.state == "off" # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[LIGHT_ON].set_value(True) - helper.characteristics[LIGHT_BRIGHTNESS].value = 100 - helper.characteristics[LIGHT_HUE].value = 4 - helper.characteristics[LIGHT_SATURATION].value = 5 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.HUE: 4, + CharacteristicsTypes.SATURATION: 5, + }, + ) assert state.state == "on" assert state.attributes["brightness"] == 255 assert state.attributes["hs_color"] == (4, 5) # Simulate that device switched off in the real world not via HA - helper.characteristics[LIGHT_ON].set_value(False) - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: False, + }, + ) assert state.state == "off" @@ -122,8 +138,8 @@ async def test_switch_push_light_state(hass, utcnow): state = hass.states.get(LIGHT_BULB_ENTITY_ID) assert state.state == "off" - await helper.update_named_service( - LIGHT_BULB_NAME, + state = await helper.async_update( + ServicesTypes.LIGHTBULB, { CharacteristicsTypes.ON: True, CharacteristicsTypes.BRIGHTNESS: 100, @@ -131,15 +147,17 @@ async def test_switch_push_light_state(hass, utcnow): CharacteristicsTypes.SATURATION: 5, }, ) - - state = hass.states.get(LIGHT_BULB_ENTITY_ID) assert state.state == "on" assert state.attributes["brightness"] == 255 assert state.attributes["hs_color"] == (4, 5) # Simulate that device switched off in the real world not via HA - await helper.update_named_service(LIGHT_BULB_NAME, {CharacteristicsTypes.ON: False}) - state = hass.states.get(LIGHT_BULB_ENTITY_ID) + state = await helper.async_update( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: False, + }, + ) assert state.state == "off" @@ -152,11 +170,14 @@ async def test_switch_read_light_state_color_temp(hass, utcnow): assert state.state == "off" # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[LIGHT_ON].set_value(True) - helper.characteristics[LIGHT_BRIGHTNESS].value = 100 - helper.characteristics[LIGHT_COLOR_TEMP].value = 400 - - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.COLOR_TEMPERATURE: 400, + }, + ) assert state.state == "on" assert state.attributes["brightness"] == 255 assert state.attributes["color_temp"] == 400 @@ -170,16 +191,14 @@ async def test_switch_push_light_state_color_temp(hass, utcnow): state = hass.states.get(LIGHT_BULB_ENTITY_ID) assert state.state == "off" - await helper.update_named_service( - LIGHT_BULB_NAME, + state = await helper.async_update( + ServicesTypes.LIGHTBULB, { CharacteristicsTypes.ON: True, CharacteristicsTypes.BRIGHTNESS: 100, CharacteristicsTypes.COLOR_TEMPERATURE: 400, }, ) - - state = hass.states.get(LIGHT_BULB_ENTITY_ID) assert state.state == "on" assert state.attributes["brightness"] == 255 assert state.attributes["color_temp"] == 400 @@ -199,12 +218,15 @@ async def test_light_becomes_unavailable_but_recovers(hass, utcnow): assert state.state == "unavailable" # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[LIGHT_ON].set_value(True) - helper.characteristics[LIGHT_BRIGHTNESS].value = 100 - helper.characteristics[LIGHT_COLOR_TEMP].value = 400 helper.pairing.available = True - - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHTBULB, + { + CharacteristicsTypes.ON: True, + CharacteristicsTypes.BRIGHTNESS: 100, + CharacteristicsTypes.COLOR_TEMPERATURE: 400, + }, + ) assert state.state == "on" assert state.attributes["brightness"] == 255 assert state.attributes["color_temp"] == 400 diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index 15e645bf18183..8f996cea8e3f4 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -4,9 +4,6 @@ from tests.components.homekit_controller.common import setup_test_component -LOCK_CURRENT_STATE = ("lock-mechanism", "lock-mechanism.current-state") -LOCK_TARGET_STATE = ("lock-mechanism", "lock-mechanism.target-state") - def create_lock_service(accessory): """Define a lock characteristics as per page 219 of HAP spec.""" @@ -35,45 +32,83 @@ async def test_switch_change_lock_state(hass, utcnow): await hass.services.async_call( "lock", "lock", {"entity_id": "lock.testdevice"}, blocking=True ) - assert helper.characteristics[LOCK_TARGET_STATE].value == 1 + helper.async_assert_service_values( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 1, + }, + ) await hass.services.async_call( "lock", "unlock", {"entity_id": "lock.testdevice"}, blocking=True ) - assert helper.characteristics[LOCK_TARGET_STATE].value == 0 + helper.async_assert_service_values( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 0, + }, + ) async def test_switch_read_lock_state(hass, utcnow): """Test that we can read the state of a HomeKit lock accessory.""" helper = await setup_test_component(hass, create_lock_service) - helper.characteristics[LOCK_CURRENT_STATE].value = 0 - helper.characteristics[LOCK_TARGET_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 0, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 0, + }, + ) assert state.state == "unlocked" assert state.attributes["battery_level"] == 50 - helper.characteristics[LOCK_CURRENT_STATE].value = 1 - helper.characteristics[LOCK_TARGET_STATE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 1, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 1, + }, + ) assert state.state == "locked" - helper.characteristics[LOCK_CURRENT_STATE].value = 2 - helper.characteristics[LOCK_TARGET_STATE].value = 1 + await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 2, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 1, + }, + ) state = await helper.poll_and_get_state() assert state.state == "jammed" - helper.characteristics[LOCK_CURRENT_STATE].value = 3 - helper.characteristics[LOCK_TARGET_STATE].value = 1 + await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 3, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 1, + }, + ) state = await helper.poll_and_get_state() assert state.state == "unknown" - helper.characteristics[LOCK_CURRENT_STATE].value = 0 - helper.characteristics[LOCK_TARGET_STATE].value = 1 + await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 0, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 1, + }, + ) state = await helper.poll_and_get_state() assert state.state == "locking" - helper.characteristics[LOCK_CURRENT_STATE].value = 1 - helper.characteristics[LOCK_TARGET_STATE].value = 0 + await helper.async_update( + ServicesTypes.LOCK_MECHANISM, + { + CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE: 1, + CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE: 0, + }, + ) state = await helper.poll_and_get_state() assert state.state == "unlocking" diff --git a/tests/components/homekit_controller/test_media_player.py b/tests/components/homekit_controller/test_media_player.py index 44c53af02daa0..d45e785f31564 100644 --- a/tests/components/homekit_controller/test_media_player.py +++ b/tests/components/homekit_controller/test_media_player.py @@ -8,11 +8,6 @@ from tests.components.homekit_controller.common import setup_test_component -CURRENT_MEDIA_STATE = ("television", "current-media-state") -TARGET_MEDIA_STATE = ("television", "target-media-state") -REMOTE_KEY = ("television", "remote-key") -ACTIVE_IDENTIFIER = ("television", "active-identifier") - def create_tv_service(accessory): """ @@ -26,10 +21,12 @@ def create_tv_service(accessory): cur_state = tv_service.add_char(CharacteristicsTypes.CURRENT_MEDIA_STATE) cur_state.value = 0 + cur_state.perms.append(CharacteristicPermissions.events) remote = tv_service.add_char(CharacteristicsTypes.REMOTE_KEY) remote.value = None remote.perms.append(CharacteristicPermissions.paired_write) + remote.perms.append(CharacteristicPermissions.events) # Add a HDMI 1 channel input_source_1 = accessory.add_service(ServicesTypes.INPUT_SOURCE) @@ -66,16 +63,28 @@ async def test_tv_read_state(hass, utcnow): """Test that we can read the state of a HomeKit fan accessory.""" helper = await setup_test_component(hass, create_tv_service) - helper.characteristics[CURRENT_MEDIA_STATE].value = 0 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 0, + }, + ) assert state.state == "playing" - helper.characteristics[CURRENT_MEDIA_STATE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 1, + }, + ) assert state.state == "paused" - helper.characteristics[CURRENT_MEDIA_STATE].value = 2 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 2, + }, + ) assert state.state == "idle" @@ -92,8 +101,12 @@ async def test_play_remote_key(hass, utcnow): """Test that we can play media on a media player.""" helper = await setup_test_component(hass, create_tv_service) - helper.characteristics[CURRENT_MEDIA_STATE].value = 1 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 1, + }, + ) await hass.services.async_call( "media_player", @@ -101,28 +114,46 @@ async def test_play_remote_key(hass, utcnow): {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value == 11 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: 11, + }, + ) # Second time should be a no-op - helper.characteristics[CURRENT_MEDIA_STATE].value = 0 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 0, + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) - helper.characteristics[REMOTE_KEY].value = None await hass.services.async_call( "media_player", "media_play", {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) async def test_pause_remote_key(hass, utcnow): """Test that we can pause a media player.""" helper = await setup_test_component(hass, create_tv_service) - helper.characteristics[CURRENT_MEDIA_STATE].value = 0 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 0, + }, + ) await hass.services.async_call( "media_player", @@ -130,28 +161,46 @@ async def test_pause_remote_key(hass, utcnow): {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value == 11 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: 11, + }, + ) # Second time should be a no-op - helper.characteristics[CURRENT_MEDIA_STATE].value = 1 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 1, + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) - helper.characteristics[REMOTE_KEY].value = None await hass.services.async_call( "media_player", "media_pause", {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) async def test_play(hass, utcnow): """Test that we can play media on a media player.""" helper = await setup_test_component(hass, create_tv_service_with_target_media_state) - helper.characteristics[CURRENT_MEDIA_STATE].value = 1 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 1, + }, + ) await hass.services.async_call( "media_player", @@ -159,30 +208,48 @@ async def test_play(hass, utcnow): {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None - assert helper.characteristics[TARGET_MEDIA_STATE].value == 0 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + CharacteristicsTypes.TARGET_MEDIA_STATE: 0, + }, + ) # Second time should be a no-op - helper.characteristics[CURRENT_MEDIA_STATE].value = 0 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 0, + CharacteristicsTypes.TARGET_MEDIA_STATE: None, + }, + ) - helper.characteristics[TARGET_MEDIA_STATE].value = None await hass.services.async_call( "media_player", "media_play", {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None - assert helper.characteristics[TARGET_MEDIA_STATE].value is None + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + CharacteristicsTypes.TARGET_MEDIA_STATE: None, + }, + ) async def test_pause(hass, utcnow): """Test that we can turn pause a media player.""" helper = await setup_test_component(hass, create_tv_service_with_target_media_state) - helper.characteristics[CURRENT_MEDIA_STATE].value = 0 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 0, + }, + ) await hass.services.async_call( "media_player", @@ -190,21 +257,35 @@ async def test_pause(hass, utcnow): {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None - assert helper.characteristics[TARGET_MEDIA_STATE].value == 1 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + CharacteristicsTypes.TARGET_MEDIA_STATE: 1, + }, + ) # Second time should be a no-op - helper.characteristics[CURRENT_MEDIA_STATE].value = 1 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 1, + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) - helper.characteristics[REMOTE_KEY].value = None await hass.services.async_call( "media_player", "media_pause", {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + }, + ) async def test_stop(hass, utcnow): @@ -217,21 +298,35 @@ async def test_stop(hass, utcnow): {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[TARGET_MEDIA_STATE].value == 2 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.TARGET_MEDIA_STATE: 2, + }, + ) # Second time should be a no-op - helper.characteristics[CURRENT_MEDIA_STATE].value = 2 - await helper.poll_and_get_state() + await helper.async_update( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.CURRENT_MEDIA_STATE: 2, + CharacteristicsTypes.TARGET_MEDIA_STATE: None, + }, + ) - helper.characteristics[TARGET_MEDIA_STATE].value = None await hass.services.async_call( "media_player", "media_stop", {"entity_id": "media_player.testdevice"}, blocking=True, ) - assert helper.characteristics[REMOTE_KEY].value is None - assert helper.characteristics[TARGET_MEDIA_STATE].value is None + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.REMOTE_KEY: None, + CharacteristicsTypes.TARGET_MEDIA_STATE: None, + }, + ) async def test_tv_set_source(hass, utcnow): @@ -244,7 +339,12 @@ async def test_tv_set_source(hass, utcnow): {"entity_id": "media_player.testdevice", "source": "HDMI 2"}, blocking=True, ) - assert helper.characteristics[ACTIVE_IDENTIFIER].value == 2 + helper.async_assert_service_values( + ServicesTypes.TELEVISION, + { + CharacteristicsTypes.ACTIVE_IDENTIFIER: 2, + }, + ) state = await helper.poll_and_get_state() assert state.attributes["source"] == "HDMI 2" diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 8eebcbda8f5d0..78bdb394f0c80 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -10,9 +10,10 @@ def create_switch_with_spray_level(accessory): service = accessory.add_service(ServicesTypes.OUTLET) spray_level = service.add_char( - CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL + CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL ) + spray_level.perms.append("ev") spray_level.value = 1 spray_level.minStep = 1 spray_level.minValue = 1 @@ -25,13 +26,32 @@ def create_switch_with_spray_level(accessory): return service +def create_switch_with_ecobee_fan_mode(accessory): + """Define battery level characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + ecobee_fan_mode = service.add_char( + CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED + ) + + ecobee_fan_mode.value = 0 + ecobee_fan_mode.minStep = 1 + ecobee_fan_mode.minValue = 0 + ecobee_fan_mode.maxValue = 100 + ecobee_fan_mode.format = "float" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + async def test_read_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) - outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. - energy_helper = Helper( + spray_level = Helper( hass, "number.testdevice_spray_quantity", helper.pairing, @@ -39,27 +59,25 @@ async def test_read_number(hass, utcnow): helper.config_entry, ) - outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - spray_level = outlet[CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL] - - state = await energy_helper.poll_and_get_state() + state = await spray_level.poll_and_get_state() assert state.state == "1" assert state.attributes["step"] == 1 assert state.attributes["min"] == 1 assert state.attributes["max"] == 5 - spray_level.value = 5 - state = await energy_helper.poll_and_get_state() + state = await spray_level.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 5}, + ) assert state.state == "5" async def test_write_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) - outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. - energy_helper = Helper( + spray_level = Helper( hass, "number.testdevice_spray_quantity", helper.pairing, @@ -67,16 +85,16 @@ async def test_write_number(hass, utcnow): helper.config_entry, ) - outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - spray_level = outlet[CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL] - await hass.services.async_call( "number", "set_value", {"entity_id": "number.testdevice_spray_quantity", "value": 5}, blocking=True, ) - assert spray_level.value == 5 + spray_level.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 5}, + ) await hass.services.async_call( "number", @@ -84,4 +102,76 @@ async def test_write_number(hass, utcnow): {"entity_id": "number.testdevice_spray_quantity", "value": 3}, blocking=True, ) - assert spray_level.value == 3 + spray_level.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3}, + ) + + +async def test_write_ecobee_fan_mode_number(hass, utcnow): + """Test a switch service that has a sensor characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_ecobee_fan_mode) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. + fan_mode = Helper( + hass, + "number.testdevice_fan_mode", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_fan_mode", "value": 1}, + blocking=True, + ) + fan_mode.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 1}, + ) + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_fan_mode", "value": 2}, + blocking=True, + ) + fan_mode.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 2}, + ) + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_fan_mode", "value": 99}, + blocking=True, + ) + fan_mode.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 99}, + ) + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_fan_mode", "value": 100}, + blocking=True, + ) + fan_mode.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 100}, + ) + + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.testdevice_fan_mode", "value": 0}, + blocking=True, + ) + fan_mode.async_assert_service_values( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 0}, + ) diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py new file mode 100644 index 0000000000000..55d5b168abea6 --- /dev/null +++ b/tests/components/homekit_controller/test_select.py @@ -0,0 +1,105 @@ +"""Basic checks for HomeKit select entities.""" +from aiohomekit.model import Accessory +from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.services import ServicesTypes + +from tests.components.homekit_controller.common import Helper, setup_test_component + + +def create_service_with_ecobee_mode(accessory: Accessory): + """Define a thermostat with ecobee mode characteristics.""" + service = accessory.add_service(ServicesTypes.THERMOSTAT, add_required=True) + + current_mode = service.add_char(CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE) + current_mode.value = 0 + current_mode.perms.append("ev") + + service.add_char(CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE) + + return service + + +async def test_read_current_mode(hass, utcnow): + """Test that Ecobee mode can be correctly read and show as human readable text.""" + helper = await setup_test_component(hass, create_service_with_ecobee_mode) + + # Helper will be for the primary entity, which is the service. Make a helper for the sensor. + ecobee_mode = Helper( + hass, + "select.testdevice_current_mode", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + state = await ecobee_mode.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: 0, + }, + ) + assert state.state == "home" + + state = await ecobee_mode.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: 1, + }, + ) + assert state.state == "sleep" + + state = await ecobee_mode.async_update( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.VENDOR_ECOBEE_CURRENT_MODE: 2, + }, + ) + assert state.state == "away" + + +async def test_write_current_mode(hass, utcnow): + """Test can set a specific mode.""" + helper = await setup_test_component(hass, create_service_with_ecobee_mode) + helper.accessory.services.first(service_type=ServicesTypes.THERMOSTAT) + + # Helper will be for the primary entity, which is the service. Make a helper for the sensor. + current_mode = Helper( + hass, + "select.testdevice_current_mode", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "home"}, + blocking=True, + ) + current_mode.async_assert_service_values( + ServicesTypes.THERMOSTAT, + {CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: 0}, + ) + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "sleep"}, + blocking=True, + ) + current_mode.async_assert_service_values( + ServicesTypes.THERMOSTAT, + {CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: 1}, + ) + + await hass.services.async_call( + "select", + "select_option", + {"entity_id": "select.testdevice_current_mode", "option": "away"}, + blocking=True, + ) + current_mode.async_assert_service_values( + ServicesTypes.THERMOSTAT, + {CharacteristicsTypes.VENDOR_ECOBEE_SET_HOLD_SCHEDULE: 2}, + ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 4c57d94b2b8e3..145d85eeed7fb 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -7,15 +7,6 @@ from tests.components.homekit_controller.common import Helper, setup_test_component -TEMPERATURE = ("temperature", "temperature.current") -HUMIDITY = ("humidity", "relative-humidity.current") -LIGHT_LEVEL = ("light", "light-level.current") -CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level") -BATTERY_LEVEL = ("battery", "battery-level") -CHARGING_STATE = ("battery", "charging-state") -LO_BATT = ("battery", "status-lo-batt") -ON = ("outlet", "on") - def create_temperature_sensor_service(accessory): """Define temperature characteristics.""" @@ -71,12 +62,20 @@ async def test_temperature_sensor_read_state(hass, utcnow): hass, create_temperature_sensor_service, suffix="temperature" ) - helper.characteristics[TEMPERATURE].value = 10 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.TEMPERATURE_SENSOR, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 10, + }, + ) assert state.state == "10" - helper.characteristics[TEMPERATURE].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.TEMPERATURE_SENSOR, + { + CharacteristicsTypes.TEMPERATURE_CURRENT: 20, + }, + ) assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE @@ -100,12 +99,20 @@ async def test_humidity_sensor_read_state(hass, utcnow): hass, create_humidity_sensor_service, suffix="humidity" ) - helper.characteristics[HUMIDITY].value = 10 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDITY_SENSOR, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 10, + }, + ) assert state.state == "10" - helper.characteristics[HUMIDITY].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.HUMIDITY_SENSOR, + { + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: 20, + }, + ) assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.HUMIDITY @@ -117,12 +124,20 @@ async def test_light_level_sensor_read_state(hass, utcnow): hass, create_light_level_sensor_service, suffix="light_level" ) - helper.characteristics[LIGHT_LEVEL].value = 10 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHT_SENSOR, + { + CharacteristicsTypes.LIGHT_LEVEL_CURRENT: 10, + }, + ) assert state.state == "10" - helper.characteristics[LIGHT_LEVEL].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.LIGHT_SENSOR, + { + CharacteristicsTypes.LIGHT_LEVEL_CURRENT: 20, + }, + ) assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.ILLUMINANCE @@ -134,12 +149,20 @@ async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): hass, create_carbon_dioxide_level_sensor_service, suffix="co2" ) - helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 10 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.CARBON_DIOXIDE_SENSOR, + { + CharacteristicsTypes.CARBON_DIOXIDE_LEVEL: 10, + }, + ) assert state.state == "10" - helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.CARBON_DIOXIDE_SENSOR, + { + CharacteristicsTypes.CARBON_DIOXIDE_LEVEL: 20, + }, + ) assert state.state == "20" @@ -149,13 +172,21 @@ async def test_battery_level_sensor(hass, utcnow): hass, create_battery_level_sensor, suffix="battery" ) - helper.characteristics[BATTERY_LEVEL].value = 100 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 100, + }, + ) assert state.state == "100" assert state.attributes["icon"] == "mdi:battery" - helper.characteristics[BATTERY_LEVEL].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 20, + }, + ) assert state.state == "20" assert state.attributes["icon"] == "mdi:battery-20" @@ -168,13 +199,21 @@ async def test_battery_charging(hass, utcnow): hass, create_battery_level_sensor, suffix="battery" ) - helper.characteristics[BATTERY_LEVEL].value = 0 - helper.characteristics[CHARGING_STATE].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 0, + CharacteristicsTypes.CHARGING_STATE: 1, + }, + ) assert state.attributes["icon"] == "mdi:battery-outline" - helper.characteristics[BATTERY_LEVEL].value = 20 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 20, + }, + ) assert state.attributes["icon"] == "mdi:battery-charging-20" @@ -184,13 +223,22 @@ async def test_battery_low(hass, utcnow): hass, create_battery_level_sensor, suffix="battery" ) - helper.characteristics[LO_BATT].value = 0 - helper.characteristics[BATTERY_LEVEL].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 1, + CharacteristicsTypes.STATUS_LO_BATT: 0, + }, + ) assert state.attributes["icon"] == "mdi:battery-10" - helper.characteristics[LO_BATT].value = 1 - state = await helper.poll_and_get_state() + state = await helper.async_update( + ServicesTypes.BATTERY_SERVICE, + { + CharacteristicsTypes.BATTERY_LEVEL: 1, + CharacteristicsTypes.STATUS_LO_BATT: 1, + }, + ) assert state.attributes["icon"] == "mdi:battery-alert" @@ -199,10 +247,11 @@ def create_switch_with_sensor(accessory): service = accessory.add_service(ServicesTypes.OUTLET) realtime_energy = service.add_char( - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY ) realtime_energy.value = 0 realtime_energy.format = "float" + realtime_energy.perms.append("ev") cur_state = service.add_char(CharacteristicsTypes.ON) cur_state.value = True @@ -213,7 +262,6 @@ def create_switch_with_sensor(accessory): async def test_switch_with_sensor(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_sensor) - outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. energy_helper = Helper( @@ -224,15 +272,20 @@ async def test_switch_with_sensor(hass, utcnow): helper.config_entry, ) - outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - realtime_energy = outlet[CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY] - - realtime_energy.value = 1 - state = await energy_helper.poll_and_get_state() + state = await energy_helper.async_update( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY: 1, + }, + ) assert state.state == "1" - realtime_energy.value = 50 - state = await energy_helper.poll_and_get_state() + state = await energy_helper.async_update( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY: 50, + }, + ) assert state.state == "50" @@ -242,7 +295,7 @@ async def test_sensor_unavailable(hass, utcnow): # Find the energy sensor and mark it as offline outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - realtime_energy = outlet[CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY] + realtime_energy = outlet[CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY] realtime_energy.status = HapStatusCode.UNABLE_TO_COMMUNICATE # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index 5c737e63edc94..9fafa4afada0a 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -42,7 +42,8 @@ def create_char_switch_service(accessory): """Define swtch characteristics.""" service = accessory.add_service(ServicesTypes.OUTLET) - on_char = service.add_char(CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE) + on_char = service.add_char(CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE) + on_char.perms.append("ev") on_char.value = False @@ -53,12 +54,22 @@ async def test_switch_change_outlet_state(hass, utcnow): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.testdevice"}, blocking=True ) - assert helper.characteristics[("outlet", "on")].value == 1 + helper.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.ON: 1, + }, + ) await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.testdevice"}, blocking=True ) - assert helper.characteristics[("outlet", "on")].value == 0 + helper.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.ON: 0, + }, + ) async def test_switch_read_outlet_state(hass, utcnow): @@ -71,19 +82,25 @@ async def test_switch_read_outlet_state(hass, utcnow): assert switch_1.attributes["outlet_in_use"] is False # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[("outlet", "on")].set_value(True) - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.ON: True}, + ) assert switch_1.state == "on" assert switch_1.attributes["outlet_in_use"] is False # Simulate that device switched off in the real world not via HA - helper.characteristics[("outlet", "on")].set_value(False) - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.ON: False}, + ) assert switch_1.state == "off" # Simulate that someone plugged something into the device - helper.characteristics[("outlet", "outlet-in-use")].value = True - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.OUTLET_IN_USE: True}, + ) assert switch_1.state == "off" assert switch_1.attributes["outlet_in_use"] is True @@ -95,12 +112,22 @@ async def test_valve_change_active_state(hass, utcnow): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.testdevice"}, blocking=True ) - assert helper.characteristics[("valve", "active")].value == 1 + helper.async_assert_service_values( + ServicesTypes.VALVE, + { + CharacteristicsTypes.ACTIVE: 1, + }, + ) await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.testdevice"}, blocking=True ) - assert helper.characteristics[("valve", "active")].value == 0 + helper.async_assert_service_values( + ServicesTypes.VALVE, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) async def test_valve_read_state(hass, utcnow): @@ -115,20 +142,24 @@ async def test_valve_read_state(hass, utcnow): assert switch_1.attributes["remaining_duration"] == 99 # Simulate that someone switched on the device in the real world not via HA - helper.characteristics[("valve", "active")].set_value(True) - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.ACTIVE: True}, + ) assert switch_1.state == "on" # Simulate that someone configured the device in the real world not via HA - helper.characteristics[ - ("valve", "is-configured") - ].value = IsConfiguredValues.NOT_CONFIGURED - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.IS_CONFIGURED: IsConfiguredValues.NOT_CONFIGURED}, + ) assert switch_1.attributes["is_configured"] is False # Simulate that someone using the device in the real world not via HA - helper.characteristics[("valve", "in-use")].value = InUseValues.NOT_IN_USE - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.VALVE, + {CharacteristicsTypes.IN_USE: InUseValues.NOT_IN_USE}, + ) assert switch_1.attributes["in_use"] is False @@ -137,8 +168,6 @@ async def test_char_switch_change_state(hass, utcnow): helper = await setup_test_component( hass, create_char_switch_service, suffix="pairing_mode" ) - svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE] await hass.services.async_call( "switch", @@ -146,7 +175,12 @@ async def test_char_switch_change_state(hass, utcnow): {"entity_id": "switch.testdevice_pairing_mode"}, blocking=True, ) - assert pairing_mode.value is True + helper.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: True, + }, + ) await hass.services.async_call( "switch", @@ -154,7 +188,12 @@ async def test_char_switch_change_state(hass, utcnow): {"entity_id": "switch.testdevice_pairing_mode"}, blocking=True, ) - assert pairing_mode.value is False + helper.async_assert_service_values( + ServicesTypes.OUTLET, + { + CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: False, + }, + ) async def test_char_switch_read_state(hass, utcnow): @@ -162,19 +201,17 @@ async def test_char_switch_read_state(hass, utcnow): helper = await setup_test_component( hass, create_char_switch_service, suffix="pairing_mode" ) - svc = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) - pairing_mode = svc[CharacteristicsTypes.Vendor.AQARA_PAIRING_MODE] - - # Initial state is that the switch is off - switch_1 = await helper.poll_and_get_state() - assert switch_1.state == "off" # Simulate that someone switched on the device in the real world not via HA - pairing_mode.set_value(True) - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: True}, + ) assert switch_1.state == "on" # Simulate that device switched off in the real world not via HA - pairing_mode.set_value(False) - switch_1 = await helper.poll_and_get_state() + switch_1 = await helper.async_update( + ServicesTypes.OUTLET, + {CharacteristicsTypes.VENDOR_AQARA_PAIRING_MODE: False}, + ) assert switch_1.state == "off" diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index f416027da4a64..d0dc7d045098a 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -8,7 +8,11 @@ from homeassistant.components import zeroconf from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from .generator import get_mock_device @@ -54,6 +58,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], port=80, hostname="p1meter-ddeeff.local.", type="", @@ -77,9 +82,19 @@ async def test_discovery_flow_works(hass, aioclient_mock): with patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, - ): + ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input=None + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): result = await hass.config_entries.flow.async_configure( - flow["flow_id"], user_input={} + flow["flow_id"], user_input={"ip_address": "192.168.43.183"} ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY @@ -126,6 +141,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], port=80, hostname="p1meter-ddeeff.local.", type="", @@ -145,6 +161,16 @@ async def test_discovery_disabled_api(hass, aioclient_mock): data=service_info, ) + assert result["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"ip_address": "192.168.43.183"} + ) + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "api_not_enabled" @@ -154,6 +180,7 @@ async def test_discovery_missing_data_in_service_info(hass, aioclient_mock): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], port=80, hostname="p1meter-ddeeff.local.", type="", @@ -182,6 +209,7 @@ async def test_discovery_invalid_api(hass, aioclient_mock): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", + addresses=["192.168.43.183"], port=80, hostname="p1meter-ddeeff.local.", type="", diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index 87a02a446e90e..02e0b5c0c23cc 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -77,7 +77,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): # Add original entry original_entry = MockConfigEntry( - domain=DOMAIN, + domain="homewizard_energy", data={CONF_IP_ADDRESS: "1.2.3.4"}, entry_id="old_id", ) @@ -122,6 +122,9 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): ) imported_entry.add_to_hass(hass) + assert imported_entry.domain == DOMAIN + assert imported_entry.domain != original_entry.domain + # Add the entry_id to trigger migration with patch( "aiohwenergy.HomeWizardEnergy", diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c03c8143badda..79d0a6c47916e 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -3,11 +3,13 @@ from http import HTTPStatus from ipaddress import ip_network import logging +import pathlib from unittest.mock import Mock, patch import pytest import homeassistant.components.http as http +from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.ssl import server_context_intermediate, server_context_modern @@ -15,6 +17,26 @@ from tests.common import async_fire_time_changed +def _setup_broken_ssl_pem_files(tmpdir): + test_dir = tmpdir.mkdir("test_broken_ssl") + cert_path = pathlib.Path(test_dir) / "cert.pem" + cert_path.write_text("garbage") + key_path = pathlib.Path(test_dir) / "key.pem" + key_path.write_text("garbage") + return cert_path, key_path + + +def _setup_empty_ssl_pem_files(tmpdir): + test_dir = tmpdir.mkdir("test_empty_ssl") + cert_path = pathlib.Path(test_dir) / "cert.pem" + cert_path.write_text("-") + peer_cert_path = pathlib.Path(test_dir) / "peer_cert.pem" + peer_cert_path.write_text("-") + key_path = pathlib.Path(test_dir) / "key.pem" + key_path.write_text("-") + return cert_path, key_path, peer_cert_path + + @pytest.fixture def mock_stack(): """Mock extract stack.""" @@ -118,60 +140,276 @@ async def test_proxy_config_only_trust_proxies(hass): ) -async def test_ssl_profile_defaults_modern(hass): +async def test_ssl_profile_defaults_modern(hass, tmpdir): """Test default ssl profile.""" - assert await async_setup_component(hass, "http", {}) is True - hass.http.ssl_certificate = "bla" + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmpdir + ) with patch("ssl.SSLContext.load_cert_chain"), patch( "homeassistant.util.ssl.server_context_modern", side_effect=server_context_modern, ) as mock_context: + assert ( + await async_setup_component( + hass, + "http", + {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}}, + ) + is True + ) await hass.async_start() await hass.async_block_till_done() assert len(mock_context.mock_calls) == 1 -async def test_ssl_profile_change_intermediate(hass): +async def test_ssl_profile_change_intermediate(hass, tmpdir): """Test setting ssl profile to intermediate.""" - assert ( - await async_setup_component( - hass, "http", {"http": {"ssl_profile": "intermediate"}} - ) - is True - ) - hass.http.ssl_certificate = "bla" + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmpdir + ) with patch("ssl.SSLContext.load_cert_chain"), patch( "homeassistant.util.ssl.server_context_intermediate", side_effect=server_context_intermediate, ) as mock_context: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "ssl_profile": "intermediate", + "ssl_certificate": cert_path, + "ssl_key": key_path, + } + }, + ) + is True + ) await hass.async_start() await hass.async_block_till_done() assert len(mock_context.mock_calls) == 1 -async def test_ssl_profile_change_modern(hass): +async def test_ssl_profile_change_modern(hass, tmpdir): """Test setting ssl profile to modern.""" - assert ( - await async_setup_component(hass, "http", {"http": {"ssl_profile": "modern"}}) - is True + + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmpdir ) - hass.http.ssl_certificate = "bla" + with patch("ssl.SSLContext.load_cert_chain"), patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_context: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "ssl_profile": "modern", + "ssl_certificate": cert_path, + "ssl_key": key_path, + } + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_context.mock_calls) == 1 + + +async def test_peer_cert(hass, tmpdir): + """Test required peer cert.""" + cert_path, key_path, peer_cert_path = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmpdir + ) with patch("ssl.SSLContext.load_cert_chain"), patch( + "ssl.SSLContext.load_verify_locations" + ) as mock_load_verify_locations, patch( "homeassistant.util.ssl.server_context_modern", side_effect=server_context_modern, ) as mock_context: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "ssl_peer_certificate": peer_cert_path, + "ssl_profile": "modern", + "ssl_certificate": cert_path, + "ssl_key": key_path, + } + }, + ) + is True + ) await hass.async_start() await hass.async_block_till_done() assert len(mock_context.mock_calls) == 1 + assert len(mock_load_verify_locations.mock_calls) == 1 + + +async def test_emergency_ssl_certificate_when_invalid(hass, tmpdir, caplog): + """Test http can startup with an emergency self signed cert when the current one is broken.""" + + cert_path, key_path = await hass.async_add_executor_job( + _setup_broken_ssl_pem_files, tmpdir + ) + + hass.config.safe_mode = True + assert ( + await async_setup_component( + hass, + "http", + { + "http": {"ssl_certificate": cert_path, "ssl_key": key_path}, + }, + ) + is True + ) + + await hass.async_start() + await hass.async_block_till_done() + assert ( + "Home Assistant is running in safe mode with an emergency self signed ssl certificate because the configured SSL certificate was not usable" + in caplog.text + ) + + assert hass.http.site is not None + + +async def test_emergency_ssl_certificate_not_used_when_not_safe_mode( + hass, tmpdir, caplog +): + """Test an emergency cert is only used in safe mode.""" + + cert_path, key_path = await hass.async_add_executor_job( + _setup_broken_ssl_pem_files, tmpdir + ) + + assert ( + await async_setup_component( + hass, "http", {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}} + ) + is False + ) + + +async def test_emergency_ssl_certificate_when_invalid_get_url_fails( + hass, tmpdir, caplog +): + """Test http falls back to no ssl when an emergency cert cannot be created when the configured one is broken. + + Ensure we can still start of we cannot determine the external url as well. + """ + cert_path, key_path = await hass.async_add_executor_job( + _setup_broken_ssl_pem_files, tmpdir + ) + hass.config.safe_mode = True + + with patch( + "homeassistant.components.http.get_url", side_effect=NoURLAvailableError + ) as mock_get_url: + assert ( + await async_setup_component( + hass, + "http", + { + "http": {"ssl_certificate": cert_path, "ssl_key": key_path}, + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_get_url.mock_calls) == 1 + assert ( + "Home Assistant is running in safe mode with an emergency self signed ssl certificate because the configured SSL certificate was not usable" + in caplog.text + ) + + assert hass.http.site is not None + + +async def test_invalid_ssl_and_cannot_create_emergency_cert(hass, tmpdir, caplog): + """Test http falls back to no ssl when an emergency cert cannot be created when the configured one is broken.""" + + cert_path, key_path = await hass.async_add_executor_job( + _setup_broken_ssl_pem_files, tmpdir + ) + hass.config.safe_mode = True + + with patch( + "homeassistant.components.http.x509.CertificateBuilder", side_effect=OSError + ) as mock_builder: + assert ( + await async_setup_component( + hass, + "http", + { + "http": {"ssl_certificate": cert_path, "ssl_key": key_path}, + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + assert "Could not create an emergency self signed ssl certificate" in caplog.text + assert len(mock_builder.mock_calls) == 1 + + assert hass.http.site is not None + + +async def test_invalid_ssl_and_cannot_create_emergency_cert_with_ssl_peer_cert( + hass, tmpdir, caplog +): + """Test http falls back to no ssl when an emergency cert cannot be created when the configured one is broken. + + When there is a peer cert verification and we cannot create + an emergency cert (probably will never happen since this means + the system is very broken), we do not want to startup http + as it would allow connections that are not verified by the cert. + """ + + cert_path, key_path = await hass.async_add_executor_job( + _setup_broken_ssl_pem_files, tmpdir + ) + hass.config.safe_mode = True + + with patch( + "homeassistant.components.http.x509.CertificateBuilder", side_effect=OSError + ) as mock_builder: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "ssl_certificate": cert_path, + "ssl_key": key_path, + "ssl_peer_certificate": cert_path, + }, + }, + ) + is False + ) + await hass.async_start() + await hass.async_block_till_done() + assert "Could not create an emergency self signed ssl certificate" in caplog.text + assert len(mock_builder.mock_calls) == 1 async def test_cors_defaults(hass): diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 0aa032ddb0da7..3c7a07ef5d6e4 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -566,6 +566,7 @@ async def test_bridge_homekit(hass, aioclient_mock): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + addresses=["0.0.0.0"], hostname="mock_hostname", name="mock_name", port=None, @@ -613,6 +614,7 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + addresses=["0.0.0.0"], hostname="mock_hostname", name="mock_name", port=None, @@ -739,6 +741,7 @@ async def test_bridge_zeroconf(hass, aioclient_mock): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.217", + addresses=["192.168.1.217"], port=443, hostname="Philips-hue.local", type="_hue._tcp.local.", @@ -772,6 +775,7 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.217", + addresses=["192.168.1.217"], port=443, hostname="Philips-hue.local", type="_hue._tcp.local.", diff --git a/tests/components/hue/test_diagnostics.py b/tests/components/hue/test_diagnostics.py new file mode 100644 index 0000000000000..8ccc91a5d195a --- /dev/null +++ b/tests/components/hue/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Test Hue diagnostics.""" + +from .conftest import setup_platform + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics_v1(hass, hass_client, mock_bridge_v1): + """Test diagnostics v1.""" + await setup_platform(hass, mock_bridge_v1, []) + config_entry = hass.config_entries.async_entries("hue")[0] + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == {} + + +async def test_diagnostics_v2(hass, hass_client, mock_bridge_v2): + """Test diagnostics v2.""" + mock_bridge_v2.api.get_diagnostics.return_value = {"hello": "world"} + await setup_platform(hass, mock_bridge_v2, []) + config_entry = hass.config_entries.async_entries("hue")[0] + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == {"hello": "world"} diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 7f5d4a569edb4..114a747590ef0 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -13,6 +13,7 @@ HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="Hunter Douglas Powerview Hub._hap._tcp.local.", port=None, @@ -22,6 +23,7 @@ ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="Hunter Douglas Powerview Hub._powerview._tcp.local.", port=None, diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 86a32877a792b..6bc2014d24e2f 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -44,13 +44,22 @@ "queries": [ { "name": "test", + "unique_id": "unique_test_id", "measurement": "measurement", "where": "where", "field": "field", } ], } -BASE_V2_QUERY = {"queries_flux": [{"name": "test", "query": "query"}]} +BASE_V2_QUERY = { + "queries_flux": [ + { + "name": "test", + "unique_id": "unique_test_id", + "query": "query", + } + ] +} @dataclass @@ -232,6 +241,7 @@ async def test_minimal_config(hass, mock_client, config_ext, queries, set_query_ "queries": [ { "name": "test", + "unique_id": "unique_test_id", "unit_of_measurement": "unit", "measurement": "measurement", "where": "where", @@ -260,6 +270,7 @@ async def test_minimal_config(hass, mock_client, config_ext, queries, set_query_ "queries_flux": [ { "name": "test", + "unique_id": "unique_test_id", "unit_of_measurement": "unit", "range_start": "start", "range_stop": "end", @@ -452,6 +463,7 @@ async def test_error_querying_influx( "queries": [ { "name": "test", + "unique_id": "unique_test_id", "measurement": "measurement", "where": "{{ illegal.template }}", "field": "field", @@ -465,7 +477,15 @@ async def test_error_querying_influx( ( API_VERSION_2, BASE_V2_CONFIG, - {"queries_flux": [{"name": "test", "query": "{{ illegal.template }}"}]}, + { + "queries_flux": [ + { + "name": "test", + "unique_id": "unique_test_id", + "query": "{{ illegal.template }}", + } + ] + }, _set_query_mock_v2, _make_v2_resultset, "query", diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 783c4c2b9e4ba..d65140dcbf9a9 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -15,6 +15,8 @@ SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS, SERVICE_SET_OPTIONS, + STORAGE_VERSION, + STORAGE_VERSION_MINOR, ) from homeassistant.const import ( ATTR_EDITABLE, @@ -25,7 +27,7 @@ SERVICE_RELOAD, ) from homeassistant.core import Context, State -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -36,11 +38,12 @@ def storage_setup(hass, hass_storage): """Storage setup.""" - async def _storage(items=None, config=None): + async def _storage(items=None, config=None, minor_version=STORAGE_VERSION_MINOR): if items is None: hass_storage[DOMAIN] = { "key": DOMAIN, - "version": 1, + "version": STORAGE_VERSION, + "minor_version": minor_version, "data": { "items": [ { @@ -55,6 +58,7 @@ async def _storage(items=None, config=None): hass_storage[DOMAIN] = { "key": DOMAIN, "version": 1, + "minor_version": minor_version, "data": {"items": items}, } if config is None: @@ -320,6 +324,46 @@ async def test_set_options_service(hass): assert state.state == "test2" +async def test_set_options_service_duplicate(hass): + """Test set_options service with duplicates.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": { + "options": ["first option", "middle option", "last option"], + "initial": "middle option", + } + } + }, + ) + entity_id = "input_select.test_1" + + state = hass.states.get(entity_id) + assert state.state == "middle option" + assert state.attributes[ATTR_OPTIONS] == [ + "first option", + "middle option", + "last option", + ] + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OPTIONS, + {ATTR_OPTIONS: ["option1", "option1"], ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == "middle option" + assert state.attributes[ATTR_OPTIONS] == [ + "first option", + "middle option", + "last option", + ] + + async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( @@ -488,6 +532,34 @@ async def test_load_from_storage(hass, storage_setup): assert state.state == "storage option 1" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) + assert state.attributes.get(ATTR_OPTIONS) == [ + "storage option 1", + "storage option 2", + ] + + +async def test_load_from_storage_duplicate(hass, storage_setup, caplog): + """Test set up from old storage with duplicates.""" + items = [ + { + "id": "from_storage", + "name": "from storage", + "options": ["yaml update 1", "yaml update 2", "yaml update 2"], + } + ] + assert await storage_setup(items, minor_version=1) + + assert ( + "Input select 'from storage' with options " + "['yaml update 1', 'yaml update 2', 'yaml update 2'] " + "had duplicated options, the duplicates have been removed" + ) in caplog.text + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "yaml update 1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + assert state.attributes.get(ATTR_OPTIONS) == ["yaml update 1", "yaml update 2"] async def test_editable_state_attribute(hass, storage_setup): @@ -554,7 +626,7 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update(hass, hass_ws_client, storage_setup): - """Test updating min/max updates the state.""" + """Test updating options updates the state.""" items = [ { @@ -590,6 +662,7 @@ async def test_update(hass, hass_ws_client, storage_setup): state = hass.states.get(input_entity_id) assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"] + # Should fail because the initial state is now invalid await client.send_json( { "id": 7, @@ -602,6 +675,46 @@ async def test_update(hass, hass_ws_client, storage_setup): assert not resp["success"] +async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog): + """Test updating options updates the state.""" + + items = [ + { + "id": "from_storage", + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + ] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_OPTIONS] == ["yaml update 1", "yaml update 2"] + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + "options": ["new option", "newer option", "newer option"], + CONF_INITIAL: "newer option", + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "unknown_error" + assert resp["error"]["message"] == "Duplicate options are not allowed" + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_OPTIONS] == ["yaml update 1", "yaml update 2"] + + async def test_ws_create(hass, hass_ws_client, storage_setup): """Test create WS.""" assert await storage_setup(items=[]) @@ -630,6 +743,38 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): state = hass.states.get(input_entity_id) assert state.state == "even newer option" + assert state.attributes[ATTR_OPTIONS] == ["new option", "even newer option"] + + +async def test_ws_create_duplicates(hass, hass_ws_client, storage_setup, caplog): + """Test create WS with duplicates.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + "options": ["new option", "even newer option", "even newer option"], + "initial": "even newer option", + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "unknown_error" + assert resp["error"]["message"] == "Duplicate options are not allowed" + + assert not hass.states.get(input_entity_id) async def test_setup_no_config(hass, hass_admin_user): diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index 95b167caba6d4..6f345522e635b 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -40,3 +40,48 @@ async def test_intent_script(hass): assert response.card["simple"]["title"] == "Hello Paulus" assert response.card["simple"]["content"] == "Content for Paulus" + + +async def test_intent_script_wait_response(hass): + """Test intent scripts work.""" + calls = async_mock_service(hass, "test", "service") + + await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "HelloWorldWaitResponse": { + "action": { + "service": "test.service", + "data_template": {"hello": "{{ name }}"}, + }, + "card": { + "title": "Hello {{ name }}", + "content": "Content for {{ name }}", + }, + "speech": {"text": "Good morning {{ name }}"}, + "reprompt": { + "text": "I didn't hear you, {{ name }}... I said good morning!" + }, + } + } + }, + ) + + response = await intent.async_handle( + hass, "test", "HelloWorldWaitResponse", {"name": {"value": "Paulus"}} + ) + + assert len(calls) == 1 + assert calls[0].data["hello"] == "Paulus" + + assert response.speech["plain"]["speech"] == "Good morning Paulus" + + assert ( + response.reprompt["plain"]["reprompt"] + == "I didn't hear you, Paulus... I said good morning!" + ) + + assert response.card["simple"]["title"] == "Hello Paulus" + assert response.card["simple"]["content"] == "Content for Paulus" diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index 0e88fb21baf22..26e2c9b338e07 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -38,6 +38,7 @@ type=IPP_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}", host=ZEROCONF_HOST, + addresses=[ZEROCONF_HOST], hostname=ZEROCONF_HOSTNAME, port=ZEROCONF_PORT, properties={"rp": ZEROCONF_RP}, @@ -47,6 +48,7 @@ type=IPPS_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}", host=ZEROCONF_HOST, + addresses=[ZEROCONF_HOST], hostname=ZEROCONF_HOSTNAME, port=ZEROCONF_PORT, properties={"rp": ZEROCONF_RP}, diff --git a/tests/components/iss/__init__.py b/tests/components/iss/__init__.py new file mode 100644 index 0000000000000..7fa75a42ccb65 --- /dev/null +++ b/tests/components/iss/__init__.py @@ -0,0 +1 @@ +"""Tests for the iss component.""" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py new file mode 100644 index 0000000000000..09f7f391b890a --- /dev/null +++ b/tests/components/iss/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test iss config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.iss.binary_sensor import DEFAULT_NAME +from homeassistant.components.iss.const import DOMAIN +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_import(hass: HomeAssistant): + """Test entry will be imported.""" + + imported_config = {CONF_NAME: DEFAULT_NAME, CONF_SHOW_ON_MAP: False} + + with patch("homeassistant.components.iss.async_setup_entry", return_value=True): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=imported_config + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("result").title == DEFAULT_NAME + assert result.get("result").options == {CONF_SHOW_ON_MAP: False} + + +async def test_create_entry(hass: HomeAssistant): + """Test we can finish a config flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + + with patch("homeassistant.components.iss.async_setup_entry", return_value=True): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("result").data == {} + + +async def test_integration_already_exists(hass: HomeAssistant): + """Test we only allow a single config flow.""" + + MockConfigEntry( + domain=DOMAIN, + data={}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={} + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_abort_no_home(hass: HomeAssistant): + """Test we don't create an entry if no coordinates are set.""" + + await async_process_ha_core_config( + hass, + {"latitude": 0.0, "longitude": 0.0}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={} + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "latitude_longitude_not_defined" + + +async def test_options(hass: HomeAssistant): + """Test options flow.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + ) + + config_entry.add_to_hass(hass) + + with patch("homeassistant.components.iss.async_setup_entry", return_value=True): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + optionflow = await hass.config_entries.options.async_init(config_entry.entry_id) + + configured = await hass.config_entries.options.async_configure( + optionflow["flow_id"], + user_input={ + CONF_SHOW_ON_MAP: True, + }, + ) + + assert configured.get("type") == "create_entry" + assert config_entry.options == {CONF_SHOW_ON_MAP: True} diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index c3aaca16d5afb..5b8b07583c539 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -17,6 +17,7 @@ UUID = "11111111-1111-1111-1111-111111111111" TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], port=8080, hostname="hostname.local.", type="_xbmc-jsonrpc-h._tcp.local.", @@ -27,6 +28,7 @@ TEST_DISCOVERY_WO_UUID = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], port=8080, hostname="hostname.local.", type="_xbmc-jsonrpc-h._tcp.local.", diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py new file mode 100644 index 0000000000000..c3ed1b4559260 --- /dev/null +++ b/tests/components/kostal_plenticore/conftest.py @@ -0,0 +1,96 @@ +"""Fixtures for Kostal Plenticore tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from kostal.plenticore import MeData, SettingsData, VersionData +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, +) -> Generator[None, MockConfigEntry, None]: + """Set up Kostal Plenticore integration for testing.""" + with patch( + "homeassistant.components.kostal_plenticore.Plenticore", autospec=True + ) as mock_api_class: + # setup + plenticore = mock_api_class.return_value + plenticore.async_setup = AsyncMock() + plenticore.async_setup.return_value = True + + plenticore.device_info = DeviceInfo( + configuration_url="http://192.168.1.2", + identifiers={("kostal_plenticore", "12345")}, + manufacturer="Kostal", + model="PLENTICORE plus 10", + name="scb", + sw_version="IOC: 01.45 MC: 01.46", + ) + + plenticore.client = MagicMock() + + plenticore.client.get_version = AsyncMock() + plenticore.client.get_version.return_value = VersionData( + { + "api_version": "0.2.0", + "hostname": "scb", + "name": "PUCK RESTful API", + "sw_version": "01.16.05025", + } + ) + + plenticore.client.get_me = AsyncMock() + plenticore.client.get_me.return_value = MeData( + { + "locked": False, + "active": True, + "authenticated": True, + "permissions": [], + "anonymous": False, + "role": "USER", + } + ) + + plenticore.client.get_process_data = AsyncMock() + plenticore.client.get_process_data.return_value = { + "devices:local": ["HomeGrid_P", "HomePv_P"] + } + + plenticore.client.get_settings = AsyncMock() + plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData( + { + "id": "Battery:MinSoc", + "unit": "%", + "default": "None", + "min": 5, + "max": 100, + "type": "byte", + "access": "readwrite", + } + ) + ] + } + + mock_config_entry = MockConfigEntry( + entry_id="2ab8dd92a62787ddfe213a67e09406bd", + title="scb", + domain="kostal_plenticore", + data={"host": "192.168.1.2", "password": "SecretPassword"}, + ) + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + yield mock_config_entry diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py new file mode 100644 index 0000000000000..56af8bafe0656 --- /dev/null +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -0,0 +1,49 @@ +"""Test Kostal Plenticore diagnostics.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry +): + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "config_entry": { + "entry_id": "2ab8dd92a62787ddfe213a67e09406bd", + "version": 1, + "domain": "kostal_plenticore", + "title": "scb", + "data": {"host": "192.168.1.2", "password": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": None, + "disabled_by": None, + }, + "client": { + "version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", + "me": "Me(locked=False, active=True, authenticated=True, permissions=[] anonymous=False role=USER)", + "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, + "available_settings_data": { + "devices:local": [ + "SettingsData(id=Battery:MinSoc, unit=%, default=None, min=5, max=100,type=byte, access=readwrite)" + ] + }, + }, + "device": { + "configuration_url": "http://192.168.1.2", + "identifiers": "**REDACTED**", + "manufacturer": "Kostal", + "model": "PLENTICORE plus 10", + "name": "scb", + "sw_version": "IOC: 01.45 MC: 01.46", + }, + } diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index 911e984a57e85..11426f20e5789 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -19,6 +19,7 @@ ZC_TYPE = "_lookin._tcp." ZEROCONF_DATA = ZeroconfServiceInfo( host=IP_ADDRESS, + addresses=[IP_ADDRESS], hostname=f"{ZC_NAME.lower()}.local.", port=80, type=ZC_TYPE, diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py new file mode 100644 index 0000000000000..d5b8e43d2bb6f --- /dev/null +++ b/tests/components/lovelace/test_cast.py @@ -0,0 +1,182 @@ +"""Test the Lovelace Cast platform.""" +from time import time +from unittest.mock import patch + +import pytest + +from homeassistant.components.lovelace import cast as lovelace_cast +from homeassistant.config import async_process_ha_core_config +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +@pytest.fixture +async def mock_https_url(hass): + """Mock valid URL.""" + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, + ) + + +@pytest.fixture +async def mock_yaml_dashboard(hass): + """Mock the content of a YAML dashboard.""" + # Set up a YAML dashboard with 2 views. + assert await async_setup_component( + hass, + "lovelace", + { + "lovelace": { + "dashboards": { + "yaml-with-views": { + "title": "YAML Title", + "mode": "yaml", + "filename": "bla.yaml", + } + } + } + }, + ) + + with patch( + "homeassistant.components.lovelace.dashboard.load_yaml", + return_value={ + "title": "YAML Title", + "views": [ + { + "title": "Hello", + }, + {"path": "second-view"}, + ], + }, + ), patch( + "homeassistant.components.lovelace.dashboard.os.path.getmtime", + return_value=time() + 10, + ): + yield + + +async def test_root_object(hass): + """Test getting a root object.""" + assert ( + await lovelace_cast.async_get_media_browser_root_object(hass, "some-type") == [] + ) + + root = await lovelace_cast.async_get_media_browser_root_object( + hass, lovelace_cast.CAST_TYPE_CHROMECAST + ) + assert len(root) == 1 + item = root[0] + assert item.title == "Lovelace" + assert item.media_class == lovelace_cast.MEDIA_CLASS_APP + assert item.media_content_id == "" + assert item.media_content_type == lovelace_cast.DOMAIN + assert item.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" + assert item.can_play is False + assert item.can_expand is True + + +async def test_browse_media_error(hass): + """Test browse media checks valid URL.""" + assert await async_setup_component(hass, "lovelace", {}) + + with pytest.raises(HomeAssistantError): + await lovelace_cast.async_browse_media( + hass, "lovelace", "", lovelace_cast.CAST_TYPE_CHROMECAST + ) + + assert ( + await lovelace_cast.async_browse_media( + hass, "not_lovelace", "", lovelace_cast.CAST_TYPE_CHROMECAST + ) + is None + ) + + +async def test_browse_media(hass, mock_yaml_dashboard, mock_https_url): + """Test browse media.""" + top_level_items = await lovelace_cast.async_browse_media( + hass, "lovelace", "", lovelace_cast.CAST_TYPE_CHROMECAST + ) + + assert len(top_level_items.children) == 2 + + child_1 = top_level_items.children[0] + assert child_1.title == "Default" + assert child_1.media_class == lovelace_cast.MEDIA_CLASS_APP + assert child_1.media_content_id == lovelace_cast.DEFAULT_DASHBOARD + assert child_1.media_content_type == lovelace_cast.DOMAIN + assert child_1.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" + assert child_1.can_play is True + assert child_1.can_expand is False + + child_2 = top_level_items.children[1] + assert child_2.title == "YAML Title" + assert child_2.media_class == lovelace_cast.MEDIA_CLASS_APP + assert child_2.media_content_id == "yaml-with-views" + assert child_2.media_content_type == lovelace_cast.DOMAIN + assert child_2.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" + assert child_2.can_play is True + assert child_2.can_expand is True + + child_2 = await lovelace_cast.async_browse_media( + hass, "lovelace", child_2.media_content_id, lovelace_cast.CAST_TYPE_CHROMECAST + ) + + assert len(child_2.children) == 2 + + grandchild_1 = child_2.children[0] + assert grandchild_1.title == "Hello" + assert grandchild_1.media_class == lovelace_cast.MEDIA_CLASS_APP + assert grandchild_1.media_content_id == "yaml-with-views/0" + assert grandchild_1.media_content_type == lovelace_cast.DOMAIN + assert ( + grandchild_1.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" + ) + assert grandchild_1.can_play is True + assert grandchild_1.can_expand is False + + grandchild_2 = child_2.children[1] + assert grandchild_2.title == "second-view" + assert grandchild_2.media_class == lovelace_cast.MEDIA_CLASS_APP + assert grandchild_2.media_content_id == "yaml-with-views/second-view" + assert grandchild_2.media_content_type == lovelace_cast.DOMAIN + assert ( + grandchild_2.thumbnail == "https://brands.home-assistant.io/_/lovelace/logo.png" + ) + assert grandchild_2.can_play is True + assert grandchild_2.can_expand is False + + with pytest.raises(HomeAssistantError): + await lovelace_cast.async_browse_media( + hass, + "lovelace", + "non-existing-dashboard", + lovelace_cast.CAST_TYPE_CHROMECAST, + ) + + +async def test_play_media(hass, mock_yaml_dashboard): + """Test playing media.""" + calls = async_mock_service(hass, "cast", "show_lovelace_view") + + await lovelace_cast.async_play_media( + hass, "media_player.my_cast", None, "lovelace", lovelace_cast.DEFAULT_DASHBOARD + ) + + assert len(calls) == 1 + assert calls[0].data["entity_id"] == "media_player.my_cast" + assert "dashboard_path" not in calls[0].data + assert calls[0].data["view_path"] == "0" + + await lovelace_cast.async_play_media( + hass, "media_player.my_cast", None, "lovelace", "yaml-with-views/second-view" + ) + + assert len(calls) == 2 + assert calls[1].data["entity_id"] == "media_player.my_cast" + assert calls[1].data["dashboard_path"] == "yaml-with-views" + assert calls[1].data["view_path"] == "second-view" diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 0e0ca8686efeb..ace4066ae3bb3 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -1 +1,42 @@ """Tests for the Lutron Caseta integration.""" + + +class MockBridge: + """Mock Lutron bridge that emulates configured connected status.""" + + def __init__(self, can_connect=True): + """Initialize MockBridge instance with configured mock connectivity.""" + self.can_connect = can_connect + self.is_currently_connected = False + self.buttons = {} + self.areas = {} + self.occupancy_groups = {} + self.scenes = self.get_scenes() + self.devices = self.get_devices() + + async def connect(self): + """Connect the mock bridge.""" + if self.can_connect: + self.is_currently_connected = True + + def is_connected(self): + """Return whether the mock bridge is connected.""" + return self.is_currently_connected + + def get_devices(self): + """Return devices on the bridge.""" + return { + "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"} + } + + def get_devices_by_domain(self, domain): + """Return devices on the bridge.""" + return {} + + def get_scenes(self): + """Return scenes on the bridge.""" + return {} + + async def close(self): + """Close the mock bridge connection.""" + self.is_currently_connected = False diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 2b947c369828a..821bf07cf0882 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -20,6 +20,8 @@ ) from homeassistant.const import CONF_HOST +from . import MockBridge + from tests.common import MockConfigEntry ATTR_HOSTNAME = "hostname" @@ -39,28 +41,6 @@ } -class MockBridge: - """Mock Lutron bridge that emulates configured connected status.""" - - def __init__(self, can_connect=True): - """Initialize MockBridge instance with configured mock connectivity.""" - self.can_connect = can_connect - self.is_currently_connected = False - - async def connect(self): - """Connect the mock bridge.""" - if self.can_connect: - self.is_currently_connected = True - - def is_connected(self): - """Return whether the mock bridge is connected.""" - return self.is_currently_connected - - async def close(self): - """Close the mock bridge connection.""" - self.is_currently_connected = False - - async def test_bridge_import_flow(hass): """Test a bridge entry gets created and set up during the import flow.""" @@ -90,6 +70,8 @@ async def test_bridge_import_flow(hass): assert result["type"] == "create_entry" assert result["title"] == CasetaConfigFlow.ENTRY_DEFAULT_TITLE assert result["data"] == entry_mock_data + assert result["result"].unique_id == "000004d2" + await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -427,6 +409,7 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="LuTrOn-abc.local.", name="mock_name", port=None, @@ -454,6 +437,7 @@ async def test_zeroconf_lutron_id_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="LuTrOn-abc.local.", name="mock_name", port=None, @@ -476,6 +460,7 @@ async def test_zeroconf_not_lutron_device(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="notlutron-abc.local.", name="mock_name", port=None, @@ -504,6 +489,7 @@ async def test_zeroconf(hass, source, tmpdir): context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="LuTrOn-abc.local.", name="mock_name", port=None, diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py new file mode 100644 index 0000000000000..89fcb65df9d70 --- /dev/null +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -0,0 +1,60 @@ +"""Test the Lutron Caseta diagnostics.""" + +from unittest.mock import patch + +from homeassistant.components.lutron_caseta import DOMAIN +from homeassistant.components.lutron_caseta.const import ( + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, +) +from homeassistant.const import CONF_HOST + +from . import MockBridge + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client) -> None: + """Test generating diagnostics for lutron_caseta.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + }, + unique_id="abc", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.lutron_caseta.Smartbridge.create_tls", + return_value=MockBridge(can_connect=True), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert diag == { + "data": { + "areas": {}, + "buttons": {}, + "devices": { + "1": { + "model": "model", + "name": "bridge", + "serial": 1234, + "type": "type", + } + }, + "occupancy_groups": {}, + "scenes": {}, + }, + "entry": { + "data": {"ca_certs": "", "certfile": "", "host": "1.1.1.1", "keyfile": ""}, + "title": "Mock Title", + }, + } diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 05033bb334742..18ed447ec5311 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -147,7 +147,7 @@ async def test_arm_home_with_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -307,7 +307,7 @@ async def test_arm_away_with_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -437,7 +437,7 @@ async def test_arm_night_with_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -510,7 +510,7 @@ async def test_trigger_no_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=60) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -557,7 +557,7 @@ async def test_trigger_with_delay(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -658,7 +658,7 @@ async def test_trigger_with_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -668,7 +668,7 @@ async def test_trigger_with_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -707,7 +707,7 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -777,7 +777,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -816,7 +816,7 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -860,7 +860,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -875,7 +875,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -918,7 +918,7 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -962,7 +962,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1010,7 +1010,7 @@ async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1059,7 +1059,7 @@ async def test_trigger_with_specific_delay(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1108,7 +1108,7 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1120,7 +1120,7 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1170,7 +1170,7 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1182,7 +1182,7 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1219,7 +1219,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1255,7 +1255,7 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1291,7 +1291,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1329,7 +1329,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1339,7 +1339,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1392,7 +1392,7 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqt future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1411,7 +1411,7 @@ async def test_arm_away_after_disabled_disarmed(hass, legacy_patchable_time, mqt future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1493,7 +1493,7 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1532,7 +1532,7 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1571,7 +1571,7 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1649,7 +1649,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1669,7 +1669,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1689,7 +1689,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Fast-forward a little bit future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual_mqtt.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual_mqtt.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py new file mode 100644 index 0000000000000..ba7a93fc3a3ce --- /dev/null +++ b/tests/components/media_player/test_browse_media.py @@ -0,0 +1,60 @@ +"""Test media browser helpers for media player.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) +from homeassistant.config import async_process_ha_core_config + + +@pytest.fixture +def mock_sign_path(): + """Mock sign path.""" + with patch( + "homeassistant.components.media_player.browse_media.async_sign_path", + side_effect=lambda _, url, _2: url + "?authSig=bla", + ): + yield + + +async def test_process_play_media_url(hass, mock_sign_path): + """Test it prefixes and signs urls.""" + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + + # Not changing a url that is not a hass url + assert ( + async_process_play_media_url(hass, "https://not-hass.com/path") + == "https://not-hass.com/path" + ) + + # Testing signing hass URLs + assert ( + async_process_play_media_url(hass, "/path") + == "http://example.local:8123/path?authSig=bla" + ) + assert ( + async_process_play_media_url(hass, "http://example.local:8123/path") + == "http://example.local:8123/path?authSig=bla" + ) + assert ( + async_process_play_media_url(hass, "http://192.168.123.123:8123/path") + == "http://192.168.123.123:8123/path?authSig=bla" + ) + + # Test skip signing URLs that have a query param + assert ( + async_process_play_media_url(hass, "/path?hello=world") + == "http://example.local:8123/path?hello=world" + ) + assert ( + async_process_play_media_url( + hass, "http://192.168.123.123:8123/path?hello=world" + ) + == "http://192.168.123.123:8123/path?hello=world" + ) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 3f0efcd45aa3b..a725129bed614 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components import media_player +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.setup import async_setup_component @@ -165,8 +166,15 @@ async def test_media_browse(hass, hass_ws_client): "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", media_player.SUPPORT_BROWSE_MEDIA, ), patch( - "homeassistant.components.media_player.MediaPlayerEntity." "async_browse_media", - return_value={"bla": "yo"}, + "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", + return_value=BrowseMedia( + media_class=media_player.MEDIA_CLASS_DIRECTORY, + media_content_id="mock-id", + media_content_type="mock-type", + title="Mock Title", + can_play=False, + can_expand=True, + ), ) as mock_browse_media: await client.send_json( { @@ -183,14 +191,25 @@ async def test_media_browse(hass, hass_ws_client): assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT assert msg["success"] - assert msg["result"] == {"bla": "yo"} + assert msg["result"] == { + "title": "Mock Title", + "media_class": "directory", + "media_content_type": "mock-type", + "media_content_id": "mock-id", + "can_play": False, + "can_expand": True, + "children_media_class": None, + "thumbnail": None, + "not_shown": 0, + "children": [], + } assert mock_browse_media.mock_calls[0][1] == ("album", "abcd") with patch( "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", media_player.SUPPORT_BROWSE_MEDIA, ), patch( - "homeassistant.components.media_player.MediaPlayerEntity." "async_browse_media", + "homeassistant.components.media_player.MediaPlayerEntity.async_browse_media", return_value={"bla": "yo"}, ): await client.send_json( diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 5b25e878e5ae7..491b1972cb680 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,12 +1,12 @@ """Test Media Source initialization.""" from unittest.mock import Mock, patch -from urllib.parse import quote import pytest +import yarl from homeassistant.components import media_source from homeassistant.components.media_player import MEDIA_CLASS_DIRECTORY, BrowseError -from homeassistant.components.media_source import const +from homeassistant.components.media_source import const, models from homeassistant.setup import async_setup_component @@ -58,9 +58,34 @@ async def test_async_browse_media(hass): assert media.title == "media" assert len(media.children) == 1, media.children media.children[0].title = "Epic Sax Guy 10 Hours" + assert media.not_shown == 1 + + # Test content filter adds to original not_shown + orig_browse = models.MediaSourceItem.async_browse + + async def not_shown_browse(self): + """Patch browsed item to set not_shown base value.""" + item = await orig_browse(self) + item.not_shown = 10 + return item + + with patch( + "homeassistant.components.media_source.models.MediaSourceItem.async_browse", + not_shown_browse, + ): + media = await media_source.async_browse_media( + hass, + "", + content_filter=lambda item: item.media_content_type.startswith("video/"), + ) + assert isinstance(media, media_source.models.BrowseMediaSource) + assert media.title == "media" + assert len(media.children) == 1, media.children + media.children[0].title = "Epic Sax Guy 10 Hours" + assert media.not_shown == 11 # Test invalid media content - with pytest.raises(ValueError): + with pytest.raises(BrowseError): await media_source.async_browse_media(hass, "invalid") # Test base URI returns all domains @@ -80,6 +105,8 @@ async def test_async_resolve_media(hass): media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"), ) assert isinstance(media, media_source.models.PlayMedia) + assert media.url == "/media/local/test.mp3" + assert media.mime_type == "audio/mpeg" async def test_async_unresolve_media(hass): @@ -91,6 +118,14 @@ async def test_async_unresolve_media(hass): with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media(hass, "") + # Test invalid media content + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media(hass, "invalid") + + # Test invalid media source + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media(hass, "media-source://media_source2") + async def test_websocket_browse_media(hass, hass_ws_client): """Test browse media websocket.""" @@ -153,7 +188,10 @@ async def test_websocket_resolve_media(hass, hass_ws_client, filename): client = await hass_ws_client(hass) - media = media_source.models.PlayMedia(f"/media/local/{filename}", "audio/mpeg") + media = media_source.models.PlayMedia( + f"/media/local/{filename}", + "audio/mpeg", + ) with patch( "homeassistant.components.media_source.async_resolve_media", @@ -171,9 +209,14 @@ async def test_websocket_resolve_media(hass, hass_ws_client, filename): assert msg["success"] assert msg["id"] == 1 - assert msg["result"]["url"].startswith(quote(media.url)) assert msg["result"]["mime_type"] == media.mime_type + # Validate url is relative and signed. + assert msg["result"]["url"][0] == "/" + parsed = yarl.URL(msg["result"]["url"]) + assert parsed.path == getattr(media, "url") + assert "authSig" in parsed.query + with patch( "homeassistant.components.media_source.async_resolve_media", side_effect=media_source.Unresolvable("test"), diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index 8a9005d7a8616..de36566fb567f 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -1,14 +1,32 @@ """Test Local Media Source.""" from http import HTTPStatus +import io +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import patch import pytest -from homeassistant.components import media_source +from homeassistant.components import media_source, websocket_api from homeassistant.components.media_source import const from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component +@pytest.fixture +async def temp_dir(hass): + """Return a temp dir.""" + with TemporaryDirectory() as tmpdirname: + target_dir = Path(tmpdirname) / "another_subdir" + target_dir.mkdir() + await async_process_ha_core_config( + hass, {"media_dirs": {"test_dir": str(target_dir)}} + ) + assert await async_setup_component(hass, const.DOMAIN, {}) + + yield str(target_dir) + + async def test_async_browse_media(hass): """Test browse media.""" local_media = hass.config.path("media") @@ -102,3 +120,214 @@ async def test_media_view(hass, hass_client): resp = await client.get("/media/recordings/test.mp3") assert resp.status == HTTPStatus.OK + + +async def test_upload_view(hass, hass_client, temp_dir, hass_admin_user): + """Allow uploading media.""" + + img = (Path(__file__).parent.parent / "image/logo.png").read_bytes() + + def get_file(name): + pic = io.BytesIO(img) + pic.name = name + return pic + + client = await hass_client() + + # Test normal upload + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir/.", + "file": get_file("logo.png"), + }, + ) + + assert res.status == 200 + assert (Path(temp_dir) / "logo.png").is_file() + + # Test with bad media source ID + for bad_id in ( + # Subdir doesn't exist + "media-source://media_source/test_dir/some-other-dir", + # Main dir doesn't exist + "media-source://media_source/test_dir2", + # Location is invalid + "media-source://media_source/test_dir/..", + # Domain != media_source + "media-source://nest/test_dir/.", + # Completely something else + "http://bla", + ): + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": bad_id, + "file": get_file("bad-source-id.png"), + }, + ) + + assert res.status == 400 + assert not (Path(temp_dir) / "bad-source-id.png").is_file() + + # Test invalid POST data + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir/.", + "file": get_file("invalid-data.png"), + "incorrect": "format", + }, + ) + + assert res.status == 400 + assert not (Path(temp_dir) / "invalid-data.png").is_file() + + # Test invalid content type + text_file = io.BytesIO(b"Hello world") + text_file.name = "hello.txt" + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir/.", + "file": text_file, + }, + ) + + assert res.status == 400 + assert not (Path(temp_dir) / "hello.txt").is_file() + + # Test invalid filename + with patch( + "aiohttp.formdata.guess_filename", return_value="../invalid-filename.png" + ): + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir/.", + "file": get_file("../invalid-filename.png"), + }, + ) + + assert res.status == 400 + assert not (Path(temp_dir) / "../invalid-filename.png").is_file() + + # Remove admin access + hass_admin_user.groups = [] + res = await client.post( + "/api/media_source/local_source/upload", + data={ + "media_content_id": "media-source://media_source/test_dir/.", + "file": get_file("no-admin-test.png"), + }, + ) + + assert res.status == 401 + assert not (Path(temp_dir) / "no-admin-test.png").is_file() + + +async def test_remove_file(hass, hass_ws_client, temp_dir, hass_admin_user): + """Allow uploading media.""" + + msg_count = 0 + file_count = 0 + + def msgid(): + nonlocal msg_count + msg_count += 1 + return msg_count + + def create_file(): + nonlocal file_count + file_count += 1 + to_delete_path = Path(temp_dir) / f"to_delete_{file_count}.txt" + to_delete_path.touch() + return to_delete_path + + client = await hass_ws_client(hass) + to_delete = create_file() + + await client.send_json( + { + "id": msgid(), + "type": "media_source/local_source/remove", + "media_content_id": f"media-source://media_source/test_dir/{to_delete.name}", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + + assert not to_delete.exists() + + # Test with bad media source ID + extra_id_file = create_file() + for bad_id, err in ( + # Not exists + ( + "media-source://media_source/test_dir/not_exist.txt", + websocket_api.ERR_NOT_FOUND, + ), + # Only a dir + ("media-source://media_source/test_dir", websocket_api.ERR_NOT_SUPPORTED), + # File with extra identifiers + ( + f"media-source://media_source/test_dir/bla/../{extra_id_file.name}", + websocket_api.ERR_INVALID_FORMAT, + ), + # Location is invalid + ("media-source://media_source/test_dir/..", websocket_api.ERR_INVALID_FORMAT), + # Domain != media_source + ("media-source://nest/test_dir/.", websocket_api.ERR_INVALID_FORMAT), + # Completely something else + ("http://bla", websocket_api.ERR_INVALID_FORMAT), + ): + await client.send_json( + { + "id": msgid(), + "type": "media_source/local_source/remove", + "media_content_id": bad_id, + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == err + + assert extra_id_file.exists() + + # Test error deleting + to_delete_2 = create_file() + + with patch("pathlib.Path.unlink", side_effect=OSError): + await client.send_json( + { + "id": msgid(), + "type": "media_source/local_source/remove", + "media_content_id": f"media-source://media_source/test_dir/{to_delete_2.name}", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == websocket_api.ERR_UNKNOWN_ERROR + + # Test requires admin access + to_delete_3 = create_file() + hass_admin_user.groups = [] + + await client.send_json( + { + "id": msgid(), + "type": "media_source/local_source/remove", + "media_content_id": f"media-source://media_source/test_dir/{to_delete_3.name}", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert to_delete_3.is_file() diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 30f6f88bd297e..24dcb5928d865 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -210,7 +210,7 @@ async def test_service_person(hass, aioclient_mock): ) aioclient_mock.delete( ENDPOINT_URL.format( - "persongroups/test_group1/persons/" "25985303-c537-4467-b41d-bdb45cd95ca1" + "persongroups/test_group1/persons/25985303-c537-4467-b41d-bdb45cd95ca1" ), status=200, text="{}", diff --git a/tests/components/mjpeg/__init__.py b/tests/components/mjpeg/__init__.py new file mode 100644 index 0000000000000..b3b796dc3b18c --- /dev/null +++ b/tests/components/mjpeg/__init__.py @@ -0,0 +1 @@ +"""Tests for the MJPEG IP Camera integration.""" diff --git a/tests/components/mjpeg/conftest.py b/tests/components/mjpeg/conftest.py new file mode 100644 index 0000000000000..c09bbde2f1d2e --- /dev/null +++ b/tests/components/mjpeg/conftest.py @@ -0,0 +1,79 @@ +"""Fixtures for MJPEG IP Camera integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest +from requests_mock import Mocker + +from homeassistant.components.mjpeg.const import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + DOMAIN, +) +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My MJPEG Camera", + domain=DOMAIN, + data={}, + options={ + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "supersecret", + CONF_STILL_IMAGE_URL: "http://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: True, + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.mjpeg.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_reload_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.mjpeg.async_reload_entry") as mock_reload: + yield mock_reload + + +@pytest.fixture +def mock_mjpeg_requests(requests_mock: Mocker) -> Generator[Mocker, None, None]: + """Fixture to provide a requests mocker.""" + requests_mock.get("https://example.com/mjpeg", text="resp") + requests_mock.get("https://example.com/still", text="resp") + yield requests_mock + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_mjpeg_requests: Mocker +) -> MockConfigEntry: + """Set up the MJPEG IP Camera integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py new file mode 100644 index 0000000000000..0678353cf6dbb --- /dev/null +++ b/tests/components/mjpeg/test_config_flow.py @@ -0,0 +1,441 @@ +"""Tests for the MJPEG IP Camera config flow.""" + +from unittest.mock import AsyncMock + +import requests +from requests_mock import Mocker + +from homeassistant.components.mjpeg.const import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Spy cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/still", + CONF_USERNAME: "frenck", + CONF_PASSWORD: "omgpuppies", + CONF_VERIFY_SSL: False, + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Spy cam" + assert result2.get("data") == {} + assert result2.get("options") == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "omgpuppies", + CONF_STILL_IMAGE_URL: "https://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_mjpeg_requests.call_count == 2 + + +async def test_full_flow_with_authentication_error( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow with invalid credentials. + + This tests tests a full config flow, with a case the user enters an invalid + credentials, but recovers by entering the correct ones. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_mjpeg_requests.get( + "https://example.com/mjpeg", text="Access Denied!", status_code=401 + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Sky cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "omgpuppies", + CONF_USERNAME: "frenck", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"username": "invalid_auth"} + assert "flow_id" in result2 + + assert len(mock_setup_entry.mock_calls) == 0 + assert mock_mjpeg_requests.call_count == 2 + + mock_mjpeg_requests.get("https://example.com/mjpeg", text="resp") + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_NAME: "Sky cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "supersecret", + CONF_USERNAME: "frenck", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Sky cam" + assert result3.get("data") == {} + assert result3.get("options") == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "supersecret", + CONF_STILL_IMAGE_URL: None, + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: True, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_mjpeg_requests.call_count == 3 + + +async def test_connection_error( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + mock_setup_entry: AsyncMock, +) -> None: + """Test connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + # Test connectione error on MJPEG url + mock_mjpeg_requests.get( + "https://example.com/mjpeg", exc=requests.exceptions.ConnectionError + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "My cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/still", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} + assert "flow_id" in result2 + + assert len(mock_setup_entry.mock_calls) == 0 + assert mock_mjpeg_requests.call_count == 1 + + # Reset + mock_mjpeg_requests.get("https://example.com/mjpeg", text="resp") + + # Test connectione error on still url + mock_mjpeg_requests.get( + "https://example.com/still", exc=requests.exceptions.ConnectionError + ) + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_NAME: "My cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/still", + }, + ) + + assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("step_id") == SOURCE_USER + assert result3.get("errors") == {"still_image_url": "cannot_connect"} + assert "flow_id" in result3 + + assert len(mock_setup_entry.mock_calls) == 0 + assert mock_mjpeg_requests.call_count == 3 + + # Reset + mock_mjpeg_requests.get("https://example.com/still", text="resp") + + # Finish + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + user_input={ + CONF_NAME: "My cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/still", + }, + ) + + assert result4.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result4.get("title") == "My cam" + assert result4.get("data") == {} + assert result4.get("options") == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "", + CONF_STILL_IMAGE_URL: "https://example.com/still", + CONF_USERNAME: None, + CONF_VERIFY_SSL: True, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_mjpeg_requests.call_count == 5 + + +async def test_already_configured( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test we abort if the MJPEG IP Camera is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "My cam", + CONF_MJPEG_URL: "https://example.com/mjpeg", + }, + ) + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "already_configured" + + +async def test_import_flow( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + mock_setup_entry: AsyncMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + CONF_MJPEG_URL: "http://example.com/mjpeg", + CONF_NAME: "Imported Camera", + CONF_PASSWORD: "omgpuppies", + CONF_STILL_IMAGE_URL: "http://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + }, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Imported Camera" + assert result.get("data") == {} + assert result.get("options") == { + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + CONF_MJPEG_URL: "http://example.com/mjpeg", + CONF_PASSWORD: "omgpuppies", + CONF_STILL_IMAGE_URL: "http://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + } + + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_mjpeg_requests.call_count == 0 + + +async def test_import_flow_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test the import configuration flow for an already configured entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_NAME: "Imported Camera", + CONF_PASSWORD: "omgpuppies", + CONF_STILL_IMAGE_URL: "https://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + }, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_options_flow( + hass: HomeAssistant, + mock_mjpeg_requests: Mocker, + init_integration: MockConfigEntry, +) -> None: + """Test options config flow.""" + result = await hass.config_entries.options.async_init(init_integration.entry_id) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + # Register a second camera + mock_mjpeg_requests.get("https://example.com/second_camera", text="resp") + mock_second_config_entry = MockConfigEntry( + title="Another Camera", + domain=DOMAIN, + data={}, + options={ + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/second_camera", + CONF_PASSWORD: "", + CONF_STILL_IMAGE_URL: None, + CONF_USERNAME: None, + CONF_VERIFY_SSL: True, + }, + ) + mock_second_config_entry.add_to_hass(hass) + + # Try updating options to already existing secondary camera + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_MJPEG_URL: "https://example.com/second_camera", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == "init" + assert result2.get("errors") == {"mjpeg_url": "already_configured"} + assert "flow_id" in result2 + + assert mock_mjpeg_requests.call_count == 1 + + # Test connectione error on MJPEG url + mock_mjpeg_requests.get( + "https://example.com/invalid_mjpeg", exc=requests.exceptions.ConnectionError + ) + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={ + CONF_MJPEG_URL: "https://example.com/invalid_mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/still", + }, + ) + + assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("step_id") == "init" + assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} + assert "flow_id" in result3 + + assert mock_mjpeg_requests.call_count == 2 + + # Test connectione error on still url + mock_mjpeg_requests.get( + "https://example.com/invalid_still", exc=requests.exceptions.ConnectionError + ) + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={ + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_STILL_IMAGE_URL: "https://example.com/invalid_still", + }, + ) + + assert result4.get("type") == RESULT_TYPE_FORM + assert result4.get("step_id") == "init" + assert result4.get("errors") == {"still_image_url": "cannot_connect"} + assert "flow_id" in result4 + + assert mock_mjpeg_requests.call_count == 4 + + # Invalid credentials + mock_mjpeg_requests.get( + "https://example.com/invalid_auth", text="Access Denied!", status_code=401 + ) + result5 = await hass.config_entries.options.async_configure( + result4["flow_id"], + user_input={ + CONF_MJPEG_URL: "https://example.com/invalid_auth", + CONF_PASSWORD: "omgpuppies", + CONF_USERNAME: "frenck", + }, + ) + + assert result5.get("type") == RESULT_TYPE_FORM + assert result5.get("step_id") == "init" + assert result5.get("errors") == {"username": "invalid_auth"} + assert "flow_id" in result5 + + assert mock_mjpeg_requests.call_count == 6 + + # Finish + result6 = await hass.config_entries.options.async_configure( + result5["flow_id"], + user_input={ + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "evenmorepuppies", + CONF_USERNAME: "newuser", + }, + ) + + assert result6.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result6.get("data") == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "https://example.com/mjpeg", + CONF_PASSWORD: "evenmorepuppies", + CONF_STILL_IMAGE_URL: None, + CONF_USERNAME: "newuser", + CONF_VERIFY_SSL: True, + } + + assert mock_mjpeg_requests.call_count == 7 diff --git a/tests/components/mjpeg/test_init.py b/tests/components/mjpeg/test_init.py new file mode 100644 index 0000000000000..853e0feb687cd --- /dev/null +++ b/tests/components/mjpeg/test_init.py @@ -0,0 +1,99 @@ +"""Tests for the MJPEG IP Camera integration.""" +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.mjpeg.const import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_mjpeg_requests: MagicMock, +) -> None: + """Test the MJPEG IP Camera configuration entry loading/unloading.""" + 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 mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_reload_config_entry( + hass: HomeAssistant, + mock_reload_entry: AsyncMock, + init_integration: MockConfigEntry, +) -> None: + """Test the MJPEG IP Camera configuration entry is reloaded on change.""" + assert len(mock_reload_entry.mock_calls) == 0 + hass.config_entries.async_update_entry( + init_integration, options={"something": "else"} + ) + assert len(mock_reload_entry.mock_calls) == 1 + + +async def test_import_config( + hass: HomeAssistant, + mock_mjpeg_requests: MagicMock, + mock_setup_entry: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test MJPEG IP Camera being set up from config via import.""" + assert await async_setup_component( + hass, + CAMERA_DOMAIN, + { + CAMERA_DOMAIN: { + "platform": DOMAIN, + CONF_MJPEG_URL: "http://example.com/mjpeg", + CONF_NAME: "Random Camera", + CONF_PASSWORD: "supersecret", + CONF_STILL_IMAGE_URL: "http://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + + assert "the MJPEG IP Camera platform in YAML is deprecated" in caplog.text + + entry = config_entries[0] + assert entry.title == "Random Camera" + assert entry.unique_id is None + assert entry.data == {} + assert entry.options == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_MJPEG_URL: "http://example.com/mjpeg", + CONF_PASSWORD: "supersecret", + CONF_STILL_IMAGE_URL: "http://example.com/still", + CONF_USERNAME: "frenck", + CONF_VERIFY_SSL: False, + } diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 5d92418bba2c2..4c4e9b54ccfdc 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -1,4 +1,5 @@ """Tests for the mobile_app HTTP API.""" +from binascii import unhexlify from http import HTTPStatus import json from unittest.mock import patch @@ -75,6 +76,49 @@ async def test_registration_encryption(hass, hass_client): assert resp.status == HTTPStatus.CREATED register_json = await resp.json() + key = unhexlify(register_json[CONF_SECRET]) + + payload = json.dumps(RENDER_TEMPLATE["data"]).encode("utf-8") + + data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await api_client.post( + f"/api/webhook/{register_json[CONF_WEBHOOK_ID]}", json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = SecretBox(key).decrypt( + webhook_json["encrypted_data"], encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + assert json.loads(decrypted_data) == {"one": "Hello world"} + + +async def test_registration_encryption_legacy(hass, hass_client): + """Test that registrations happen.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + api_client = await hass_client() + + resp = await api_client.post("/api/mobile_app/registrations", json=REGISTER) + + assert resp.status == HTTPStatus.CREATED + register_json = await resp.json() + keylen = SecretBox.KEY_SIZE key = register_json[CONF_SECRET].encode("utf-8") key = key[:keylen] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 48b61988de20c..5f220cf0ebe82 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,5 @@ """Webhook tests for mobile_app.""" +from binascii import unhexlify from http import HTTPStatus from unittest.mock import patch @@ -22,7 +23,29 @@ from tests.common import async_mock_service -def encrypt_payload(secret_key, payload): +def encrypt_payload(secret_key, payload, encode_json=True): + """Return a encrypted payload given a key and dictionary of data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + prepped_key = unhexlify(secret_key) + + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") + + return ( + SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + ) + + +def encrypt_payload_legacy(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" try: from nacl.encoding import Base64Encoder @@ -38,7 +61,9 @@ def encrypt_payload(secret_key, payload): prepped_key = prepped_key[:keylen] prepped_key = prepped_key.ljust(keylen, b"\0") - payload = json.dumps(payload).encode("utf-8") + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") return ( SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") @@ -56,6 +81,27 @@ def decrypt_payload(secret_key, encrypted_data): import json + prepped_key = unhexlify(secret_key) + + decrypted_data = SecretBox(prepped_key).decrypt( + encrypted_data, encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + return json.loads(decrypted_data) + + +def decrypt_payload_legacy(secret_key, encrypted_data): + """Return a decrypted payload given a key and a string of encrypted data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] @@ -273,6 +319,181 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations): assert decrypted_data == {"one": "Hello world"} +async def test_webhook_handle_decryption_legacy(webhook_client, create_registrations): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + +async def test_webhook_handle_decryption_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload_legacy(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload_legacy(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_upgrade( + webhook_client, create_registrations +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using new method + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using legacy method - no longer possible + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + # The response should be empty, encrypted with the new method + with pytest.raises(Exception): + decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {} + + async def test_webhook_requires_encryption(webhook_client, create_registrations): """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 91327b4e2a23a..b00f9700f7f28 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -9,14 +9,14 @@ import pytest from homeassistant.components.modbus.const import MODBUS_DOMAIN as DOMAIN, TCP -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SLAVE, CONF_TYPE from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, mock_restore_cache TEST_MODBUS_NAME = "modbusTest" -TEST_ENTITY_NAME = "test_entity" +TEST_ENTITY_NAME = "test entity" TEST_MODBUS_HOST = "modbusHost" TEST_PORT_TCP = 5501 TEST_PORT_SERIAL = "usb01" @@ -82,9 +82,12 @@ async def mock_modbus_fixture( ): """Load integration modbus using mocked pymodbus.""" conf = copy.deepcopy(do_config) - if config_addon: - for key in conf.keys(): + for key in conf.keys(): + if config_addon: conf[key][0].update(config_addon) + for entity in conf[key]: + if CONF_SLAVE not in entity: + entity[CONF_SLAVE] = 0 caplog.set_level(logging.WARNING) config = { DOMAIN: [ diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 5127bd55ad1c4..15a03b76927fd 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -7,6 +7,7 @@ CALL_TYPE_DISCRETE, CONF_INPUT_TYPE, CONF_LAZY_ERROR, + CONF_SLAVE_COUNT, ) from homeassistant.const import ( CONF_ADDRESS, @@ -24,7 +25,7 @@ from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle -ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") @pytest.mark.parametrize( @@ -188,9 +189,17 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): assert hass.states.get(ENTITY_ID).state == STATE_ON +ENTITY_ID2 = f"{ENTITY_ID}_1" + + @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, STATE_ON),)], + [ + ( + State(ENTITY_ID, STATE_ON), + State(ENTITY_ID2, STATE_OFF), + ) + ], indirect=True, ) @pytest.mark.parametrize( @@ -202,6 +211,7 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SCAN_INTERVAL: 0, + CONF_SLAVE_COUNT: 1, } ] }, @@ -210,3 +220,100 @@ async def test_service_binary_sensor_update(hass, mock_modbus, mock_ha): async def test_restore_state_binary_sensor(hass, mock_test_state, mock_modbus): """Run test for binary sensor restore state.""" assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state + assert hass.states.get(ENTITY_ID2).state == mock_test_state[1].state + + +TEST_NAME = "test_sensor" + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SLAVE_COUNT: 3, + } + ] + }, + ], +) +async def test_config_slave_binary_sensor(hass, mock_modbus): + """Run config test for binary sensor.""" + assert SENSOR_DOMAIN in hass.config.components + + for addon in ["", " 1", " 2", " 3"]: + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}{addon}".replace(" ", "_") + assert hass.states.get(entity_id) is not None + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SLAVE_COUNT: 8, + } + ] + }, + ], +) +@pytest.mark.parametrize( + "register_words,expected, slaves", + [ + ( + [0x01, 0x00], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + ], + ), + ( + [0x02, 0x00], + STATE_OFF, + [ + STATE_ON, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + ], + ), + ( + [0x01, 0x01], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_ON, + ], + ), + ], +) +async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle): + """Run test for given config.""" + assert hass.states.get(ENTITY_ID).state == expected + + for i in range(8): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") + assert hass.states.get(entity_id).state == slaves[i] diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index db471709c10b5..80c8590f7ccdb 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -22,7 +22,7 @@ from .conftest import TEST_ENTITY_NAME, ReadResult -ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index cc879b2c1689a..6797dc8713cab 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -32,8 +32,8 @@ from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle -ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}" -ENTITY_ID2 = f"{ENTITY_ID}2" +ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +ENTITY_ID2 = f"{ENTITY_ID}_2" @pytest.mark.parametrize( @@ -270,7 +270,7 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME} 2", CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_ADDRESS: 1235, CONF_SCAN_INTERVAL: 0, diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 9ffa48a032a39..9b0564504d92a 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -16,29 +16,24 @@ CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, - TCP, ) from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, - CONF_HOST, CONF_NAME, - CONF_PORT, CONF_SCAN_INTERVAL, CONF_SLAVE, - CONF_TYPE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import State -from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY_NAME, TEST_MODBUS_HOST, TEST_PORT_TCP, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult -ENTITY_ID = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}" -ENTITY_ID2 = f"{ENTITY_ID}2" +ENTITY_ID = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +ENTITY_ID2 = f"{ENTITY_ID}_2" @pytest.mark.parametrize( @@ -221,14 +216,10 @@ async def test_restore_state_fan(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == STATE_ON -async def test_fan_service_turn(hass, caplog, mock_pymodbus): - """Run test for service turn_on/turn_off.""" - - config = { - MODBUS_DOMAIN: { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, +@pytest.mark.parametrize( + "do_config", + [ + { CONF_FANS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -237,7 +228,7 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME} 2", CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -245,9 +236,11 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): }, ], }, - } - assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True - await hass.async_block_till_done() + ], +) +async def test_fan_service_turn(hass, caplog, mock_modbus): + """Run test for service turn_on/turn_off.""" + assert MODBUS_DOMAIN in hass.config.components assert hass.states.get(ENTITY_ID).state == STATE_OFF @@ -262,27 +255,27 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( "fan", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_ON - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "fan", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_OFF - mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + mock_modbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( "fan", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE - mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + mock_modbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( "fan", "turn_off", service_data={"entity_id": ENTITY_ID} ) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 7d9ab3e347186..9bd0a2caa6d3b 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -76,6 +76,7 @@ CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, + CONF_SLAVE, CONF_STRUCTURE, CONF_TIMEOUT, CONF_TYPE, @@ -233,7 +234,7 @@ async def test_exception_struct_validator(do_config): { CONF_NAME: TEST_MODBUS_NAME, CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST + "2", + CONF_HOST: TEST_MODBUS_HOST + " 2", CONF_PORT: TEST_PORT_TCP, }, ], @@ -245,7 +246,7 @@ async def test_exception_struct_validator(do_config): CONF_PORT: TEST_PORT_TCP, }, { - CONF_NAME: TEST_MODBUS_NAME + "2", + CONF_NAME: TEST_MODBUS_NAME + " 2", CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, @@ -272,10 +273,12 @@ async def test_duplicate_modbus_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, + CONF_SLAVE: 0, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 119, + CONF_SLAVE: 0, }, ], } @@ -290,10 +293,12 @@ async def test_duplicate_modbus_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, + CONF_SLAVE: 0, }, { - CONF_NAME: TEST_ENTITY_NAME + "2", + CONF_NAME: TEST_ENTITY_NAME + " 2", CONF_ADDRESS: 117, + CONF_SLAVE: 0, }, ], } @@ -387,7 +392,7 @@ async def test_duplicate_entity_validator(do_config): CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, - CONF_NAME: f"{TEST_MODBUS_NAME}2", + CONF_NAME: f"{TEST_MODBUS_NAME} 2", }, { CONF_TYPE: SERIAL, @@ -397,7 +402,7 @@ async def test_duplicate_entity_validator(do_config): CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, - CONF_NAME: f"{TEST_MODBUS_NAME}3", + CONF_NAME: f"{TEST_MODBUS_NAME} 3", }, ], { @@ -409,6 +414,7 @@ async def test_duplicate_entity_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, + CONF_SLAVE: 0, CONF_SCAN_INTERVAL: 0, } ], @@ -544,6 +550,7 @@ async def mock_modbus_read_pymodbus_fixture( CONF_INPUT_TYPE: do_type, CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_SLAVE: 0, CONF_SCAN_INTERVAL: do_scan_interval, } ], @@ -592,7 +599,7 @@ async def test_pb_read( """Run test for different read.""" # Check state - entity_id = f"{do_domain}.{TEST_ENTITY_NAME}" + entity_id = f"{do_domain}.{TEST_ENTITY_NAME}".replace(" ", "_") state = hass.states.get(entity_id).state assert hass.states.get(entity_id).state @@ -674,7 +681,7 @@ async def test_delay(hass, mock_pymodbus): # We "hijiack" a binary_sensor to make a proper blackbox test. set_delay = 15 set_scan_interval = 5 - entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") config = { DOMAIN: [ { @@ -688,6 +695,7 @@ async def test_delay(hass, mock_pymodbus): CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 52, + CONF_SLAVE: 0, CONF_SCAN_INTERVAL: set_scan_interval, }, ], @@ -736,6 +744,7 @@ async def test_delay(hass, mock_pymodbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 117, + CONF_SLAVE: 0, CONF_SCAN_INTERVAL: 0, } ], @@ -759,6 +768,7 @@ async def test_shutdown(hass, caplog, mock_pymodbus, mock_modbus_with_pymodbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_SLAVE: 0, } ] }, @@ -768,7 +778,7 @@ async def test_stop_restart(hass, caplog, mock_modbus): """Run test for service stop.""" caplog.set_level(logging.INFO) - entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") assert hass.states.get(entity_id).state == STATE_UNKNOWN hass.states.async_set(entity_id, 17) await hass.async_block_till_done() diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 451d0beca1391..f98f1105fa002 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -15,30 +15,25 @@ CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, - TCP, ) from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, - CONF_HOST, CONF_LIGHTS, CONF_NAME, - CONF_PORT, CONF_SCAN_INTERVAL, CONF_SLAVE, - CONF_TYPE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import State -from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY_NAME, TEST_MODBUS_HOST, TEST_PORT_TCP, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult -ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_ENTITY_NAME}" -ENTITY_ID2 = f"{ENTITY_ID}2" +ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +ENTITY_ID2 = f"{ENTITY_ID}_2" @pytest.mark.parametrize( @@ -221,14 +216,10 @@ async def test_restore_state_light(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state -async def test_light_service_turn(hass, caplog, mock_pymodbus): - """Run test for service turn_on/turn_off.""" - - config = { - MODBUS_DOMAIN: { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, +@pytest.mark.parametrize( + "do_config", + [ + { CONF_LIGHTS: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -237,7 +228,7 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME} 2", CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -245,9 +236,11 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): }, ], }, - } - assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True - await hass.async_block_till_done() + ], +) +async def test_light_service_turn(hass, caplog, mock_modbus): + """Run test for service turn_on/turn_off.""" + assert MODBUS_DOMAIN in hass.config.components assert hass.states.get(ENTITY_ID).state == STATE_OFF @@ -262,27 +255,27 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( "light", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_ON - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "light", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_OFF - mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + mock_modbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( "light", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE - mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + mock_modbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( "light", "turn_off", service_data={"entity_id": ENTITY_ID} ) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 0edd9bdc945e7..053dc46c6ba35 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -37,7 +37,7 @@ from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle -ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") @pytest.mark.parametrize( diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 15a41956d3fec..006e7ee8d15b8 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -17,7 +17,6 @@ CONF_VERIFY, CONF_WRITE_TYPE, MODBUS_DOMAIN, - TCP, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -26,33 +25,23 @@ CONF_COMMAND_ON, CONF_DELAY, CONF_DEVICE_CLASS, - CONF_HOST, CONF_NAME, - CONF_PORT, CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_SWITCHES, - CONF_TYPE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import State -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import ( - TEST_ENTITY_NAME, - TEST_MODBUS_HOST, - TEST_PORT_TCP, - ReadResult, - do_next_cycle, -) +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle from tests.common import async_fire_time_changed -ENTITY_ID = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}" -ENTITY_ID2 = f"{ENTITY_ID}2" +ENTITY_ID = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +ENTITY_ID2 = f"{ENTITY_ID}_2" @pytest.mark.parametrize( @@ -280,14 +269,10 @@ async def test_restore_state_switch(hass, mock_test_state, mock_modbus): assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state -async def test_switch_service_turn(hass, caplog, mock_pymodbus): - """Run test for service turn_on/turn_off.""" - - config = { - MODBUS_DOMAIN: { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, +@pytest.mark.parametrize( + "do_config", + [ + { CONF_SWITCHES: [ { CONF_NAME: TEST_ENTITY_NAME, @@ -296,7 +281,7 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): CONF_SCAN_INTERVAL: 0, }, { - CONF_NAME: f"{TEST_ENTITY_NAME}2", + CONF_NAME: f"{TEST_ENTITY_NAME} 2", CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, @@ -304,9 +289,10 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): }, ], }, - } - assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True - await hass.async_block_till_done() + ], +) +async def test_switch_service_turn(hass, caplog, mock_modbus): + """Run test for service turn_on/turn_off.""" assert MODBUS_DOMAIN in hass.config.components assert hass.states.get(ENTITY_ID).state == STATE_OFF @@ -321,27 +307,27 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_OFF - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) assert hass.states.get(ENTITY_ID2).state == STATE_OFF await hass.services.async_call( "switch", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_ON - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x00]) await hass.services.async_call( "switch", "turn_off", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_OFF - mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + mock_modbus.write_register.side_effect = ModbusException("fail write_") await hass.services.async_call( "switch", "turn_on", service_data={"entity_id": ENTITY_ID2} ) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE - mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + mock_modbus.write_coil.side_effect = ModbusException("fail write_") await hass.services.async_call( "switch", "turn_off", service_data={"entity_id": ENTITY_ID} ) @@ -377,33 +363,28 @@ async def test_service_switch_update(hass, mock_modbus, mock_ha): assert hass.states.get(ENTITY_ID).state == STATE_ON -async def test_delay_switch(hass, mock_pymodbus): +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SWITCHES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, + CONF_VERIFY: { + CONF_DELAY: 1, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + } + ], + }, + ], +) +async def test_delay_switch(hass, mock_modbus): """Run test for switch verify delay.""" - config = { - MODBUS_DOMAIN: [ - { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, - CONF_SWITCHES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 51, - CONF_SCAN_INTERVAL: 0, - CONF_VERIFY: { - CONF_DELAY: 1, - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - }, - } - ], - } - ] - } - mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True - await hass.async_block_till_done() await hass.services.async_call( "switch", "turn_on", service_data={"entity_id": ENTITY_ID} ) diff --git a/tests/components/modem_callerid/__init__.py b/tests/components/modem_callerid/__init__.py index 9564f2b662fce..419f5bc50ff9a 100644 --- a/tests/components/modem_callerid/__init__.py +++ b/tests/components/modem_callerid/__init__.py @@ -3,24 +3,29 @@ from unittest.mock import patch from phone_modem import DEFAULT_PORT +from serial.tools.list_ports_common import ListPortInfo -from homeassistant.const import CONF_DEVICE -CONF_DATA = {CONF_DEVICE: DEFAULT_PORT} - -IMPORT_DATA = {"sensor": {"platform": "modem_callerid"}} - - -def _patch_init_modem(): +def patch_init_modem(): + """Mock modem.""" return patch( - "homeassistant.components.modem_callerid.PhoneModem", - autospec=True, + "homeassistant.components.modem_callerid.PhoneModem.initialize", ) -def _patch_config_flow_modem(mocked_modem): +def patch_config_flow_modem(): + """Mock modem config flow.""" return patch( - "homeassistant.components.modem_callerid.config_flow.PhoneModem", - autospec=True, - return_value=mocked_modem, + "homeassistant.components.modem_callerid.config_flow.PhoneModem.test", ) + + +def com_port(): + """Mock of a serial port.""" + port = ListPortInfo(DEFAULT_PORT) + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = DEFAULT_PORT + port.description = "Some serial port" + + return port diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 0956a8fe1b795..19a98106c63ed 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -1,21 +1,16 @@ """Test Modem Caller ID config flow.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import phone_modem -import serial.tools.list_ports +from homeassistant import data_entry_flow from homeassistant.components import usb from homeassistant.components.modem_callerid.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) -from . import _patch_config_flow_modem +from . import com_port, patch_config_flow_modem DISCOVERY_INFO = usb.UsbServiceInfo( device=phone_modem.DEFAULT_PORT, @@ -30,51 +25,38 @@ def _patch_setup(): return patch( "homeassistant.components.modem_callerid.async_setup_entry", - return_value=True, ) -def com_port(): - """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo(phone_modem.DEFAULT_PORT) - port.serial_number = "1234" - port.manufacturer = "Virtual serial port" - port.device = phone_modem.DEFAULT_PORT - port.description = "Some serial port" - - return port - - @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) async def test_flow_usb(hass: HomeAssistant): """Test usb discovery flow.""" - port = com_port() - with _patch_config_flow_modem(AsyncMock()), _patch_setup(): + with patch_config_flow_modem(), _patch_setup(): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {CONF_DEVICE: port.device} + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_DEVICE: com_port().device} @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) async def test_flow_usb_cannot_connect(hass: HomeAssistant): """Test usb flow connection error.""" - with _patch_config_flow_modem(AsyncMock()) as modemmock: + with patch_config_flow_modem() as modemmock: modemmock.side_effect = phone_modem.exceptions.SerialError result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" @@ -90,14 +72,13 @@ async def test_flow_user(hass: HomeAssistant): port.vid, port.pid, ) - mocked_modem = AsyncMock() - with _patch_config_flow_modem(mocked_modem), _patch_setup(): + with patch_config_flow_modem(), _patch_setup(): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -105,7 +86,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" @@ -121,12 +102,12 @@ async def test_flow_user_error(hass: HomeAssistant): port.vid, port.pid, ) - with _patch_config_flow_modem(AsyncMock()) as modemmock: + with patch_config_flow_modem() as modemmock: modemmock.side_effect = phone_modem.exceptions.SerialError result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -135,32 +116,32 @@ async def test_flow_user_error(hass: HomeAssistant): result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @patch("serial.tools.list_ports.comports", MagicMock()) async def test_flow_user_no_port_list(hass: HomeAssistant): """Test user with no list of ports.""" - with _patch_config_flow_modem(AsyncMock()): + with patch_config_flow_modem(): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" async def test_abort_user_with_existing_flow(hass: HomeAssistant): """Test user flow is aborted when another discovery has happened.""" - with _patch_config_flow_modem(AsyncMock()): + with patch_config_flow_modem(): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -169,5 +150,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): data={}, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index f467ca5af514b..0465fb24a0752 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -1,25 +1,29 @@ """Test Modem Caller ID integration.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from phone_modem import exceptions from homeassistant.components.modem_callerid.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant -from . import CONF_DATA, _patch_init_modem +from . import com_port, patch_init_modem from tests.common import MockConfigEntry -async def test_setup_config(hass: HomeAssistant): - """Test Modem Caller ID setup.""" +async def test_setup_entry(hass: HomeAssistant): + """Test Modem Caller ID entry setup.""" entry = MockConfigEntry( domain=DOMAIN, - data=CONF_DATA, + data={CONF_DEVICE: com_port().device}, ) entry.add_to_hass(hass) - with _patch_init_modem(): + with patch("aioserial.AioSerial", return_value=AsyncMock()), patch( + "homeassistant.components.modem_callerid.PhoneModem._get_response", + return_value="OK", + ), patch("phone_modem.PhoneModem._modem_sm"): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED @@ -28,28 +32,26 @@ async def test_async_setup_entry_not_ready(hass: HomeAssistant): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" entry = MockConfigEntry( domain=DOMAIN, - data=CONF_DATA, + data={CONF_DEVICE: com_port().device}, ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.modem_callerid.PhoneModem", - side_effect=exceptions.SerialError(), - ): + with patch_init_modem() as modemmock: + modemmock.side_effect = exceptions.SerialError await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ConfigEntryState.SETUP_ERROR + assert entry.state == ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) -async def test_unload_config_entry(hass: HomeAssistant): +async def test_unload_entry(hass: HomeAssistant): """Test unload.""" entry = MockConfigEntry( domain=DOMAIN, - data=CONF_DATA, + data={CONF_DEVICE: com_port().device}, ) entry.add_to_hass(hass) - with _patch_init_modem(): + with patch_init_modem(): await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index b1b2eb618afb8..931d2918fe2ab 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -71,6 +71,7 @@ async def test_full_zeroconf_flow_implementation( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -140,6 +141,7 @@ async def test_zeroconf_connection_error( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -171,6 +173,7 @@ async def test_zeroconf_confirm_connection_error( }, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.com.", name="mock_name", port=None, @@ -240,6 +243,7 @@ async def test_zeroconf_with_mac_device_exists_abort( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, diff --git a/tests/components/moehlenhoff_alpha2/__init__.py b/tests/components/moehlenhoff_alpha2/__init__.py new file mode 100644 index 0000000000000..76bd1fd00aad0 --- /dev/null +++ b/tests/components/moehlenhoff_alpha2/__init__.py @@ -0,0 +1 @@ +"""Tests for the moehlenhoff_alpha2 integration.""" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py new file mode 100644 index 0000000000000..ccfa98718e572 --- /dev/null +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the moehlenhoff_alpha2 config flow.""" +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.moehlenhoff_alpha2.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + +MOCK_BASE_ID = "fake-base-id" +MOCK_BASE_NAME = "fake-base-name" +MOCK_BASE_HOST = "fake-base-host" + + +async def mock_update_data(self): + """Mock moehlenhoff_alpha2.Alpha2Base.update_data.""" + self.static_data = { + "Devices": { + "Device": {"ID": MOCK_BASE_ID, "NAME": MOCK_BASE_NAME, "HEATAREA": []} + } + } + + +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"] == RESULT_TYPE_FORM + assert not result["errors"] + + with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), patch( + "homeassistant.components.moehlenhoff_alpha2.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={"host": MOCK_BASE_HOST}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == MOCK_BASE_NAME + assert result2["data"] == {"host": MOCK_BASE_HOST} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicate_error(hass: HomeAssistant) -> None: + """Test that errors are shown when duplicates are added.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": MOCK_BASE_HOST}, + source=config_entries.SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert config_entry.data["host"] == MOCK_BASE_HOST + + with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": MOCK_BASE_HOST}, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: + """Test connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "moehlenhoff_alpha2.Alpha2Base.update_data", side_effect=asyncio.TimeoutError + ): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={"host": MOCK_BASE_HOST}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected_error(hass: HomeAssistant) -> None: + """Test unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch("moehlenhoff_alpha2.Alpha2Base.update_data", side_effect=Exception): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={"host": MOCK_BASE_HOST}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 8a0269ba9fecd..066620b1051bb 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,56 +1,66 @@ """The test for the moon sensor platform.""" -from datetime import datetime +from __future__ import annotations + from unittest.mock import patch +import pytest + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) +from homeassistant.components.moon.sensor import ( + MOON_ICONS, + STATE_FIRST_QUARTER, + STATE_FULL_MOON, + STATE_LAST_QUARTER, + STATE_NEW_MOON, + STATE_WANING_CRESCENT, + STATE_WANING_GIBBOUS, + STATE_WAXING_CRESCENT, + STATE_WAXING_GIBBOUS, +) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util -DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) -DAY2 = datetime(2017, 1, 18, 1, tzinfo=dt_util.UTC) - -async def test_moon_day1(hass): - """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon", "name": "moon_day1"}} - - await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.states.get("sensor.moon_day1") - - with patch( - "homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY1 - ): - await async_update_entity(hass, "sensor.moon_day1") - - assert hass.states.get("sensor.moon_day1").state == "waxing_crescent" - - -async def test_moon_day2(hass): +@pytest.mark.parametrize( + "moon_value,native_value,icon", + [ + (0, STATE_NEW_MOON, MOON_ICONS[STATE_NEW_MOON]), + (5, STATE_WAXING_CRESCENT, MOON_ICONS[STATE_WAXING_CRESCENT]), + (7, STATE_FIRST_QUARTER, MOON_ICONS[STATE_FIRST_QUARTER]), + (12, STATE_WAXING_GIBBOUS, MOON_ICONS[STATE_WAXING_GIBBOUS]), + (14.3, STATE_FULL_MOON, MOON_ICONS[STATE_FULL_MOON]), + (20.1, STATE_WANING_GIBBOUS, MOON_ICONS[STATE_WANING_GIBBOUS]), + (20.8, STATE_LAST_QUARTER, MOON_ICONS[STATE_LAST_QUARTER]), + (23, STATE_WANING_CRESCENT, MOON_ICONS[STATE_WANING_CRESCENT]), + ], +) +async def test_moon_day( + hass: HomeAssistant, moon_value: float, native_value: str, icon: str +) -> None: """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon", "name": "moon_day2"}} + config = {"sensor": {"platform": "moon"}} await async_setup_component(hass, HA_DOMAIN, {}) assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert hass.states.get("sensor.moon_day2") + assert hass.states.get("sensor.moon") with patch( - "homeassistant.components.moon.sensor.dt_util.utcnow", return_value=DAY2 + "homeassistant.components.moon.sensor.moon.phase", return_value=moon_value ): - await async_update_entity(hass, "sensor.moon_day2") + await async_update_entity(hass, "sensor.moon") - assert hass.states.get("sensor.moon_day2").state == "waning_gibbous" + state = hass.states.get("sensor.moon") + assert state.state == native_value + assert state.attributes["icon"] == icon -async def async_update_entity(hass, entity_id): +async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Run an update action for an entity.""" await hass.services.async_call( HA_DOMAIN, diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index f2c8db3879b03..6979d5c645dde 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.motioneye.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import ( TEST_CAMERA_DEVICE_IDENTIFIER, @@ -67,6 +68,12 @@ _LOGGER = logging.getLogger(__name__) +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + async def test_async_browse_media_success(hass: HomeAssistant) -> None: """Test successful browse media.""" @@ -103,10 +110,11 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": True, - "children_media_class": "directory", "thumbnail": None, + "children_media_class": "directory", } ], + "not_shown": 0, } media = await media_source.async_browse_media( @@ -135,10 +143,11 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": True, - "children_media_class": "directory", "thumbnail": None, + "children_media_class": "directory", } ], + "not_shown": 0, } media = await media_source.async_browse_media( @@ -166,8 +175,8 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": True, - "children_media_class": "video", "thumbnail": None, + "children_media_class": "video", }, { "title": "Images", @@ -179,10 +188,11 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": True, - "children_media_class": "image", "thumbnail": None, + "children_media_class": "image", }, ], + "not_shown": 0, } client.async_get_movies = AsyncMock(return_value=TEST_MOVIES) @@ -213,10 +223,11 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": True, - "children_media_class": "directory", "thumbnail": None, + "children_media_class": "directory", } ], + "not_shown": 0, } client.get_movie_url = Mock(return_value="http://movie") @@ -248,8 +259,8 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-36-49.mp4", @@ -262,8 +273,8 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-02-27.mp4", @@ -276,10 +287,11 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: ), "can_play": True, "can_expand": False, - "children_media_class": None, "thumbnail": "http://movie", + "children_media_class": None, }, ], + "not_shown": 0, } @@ -326,10 +338,11 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: ), "can_play": False, "can_expand": False, - "children_media_class": None, "thumbnail": "http://image", + "children_media_class": None, } ], + "not_shown": 0, } @@ -479,4 +492,5 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: "children_media_class": "video", "thumbnail": None, "children": [], + "not_shown": 0, } diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 16e46faaef856..a278cde768b25 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -53,6 +53,7 @@ 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, @@ -771,7 +772,12 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + alarm_control_panel.SERVICE_ALARM_DISARM, + command_payload="DISARM", ) @@ -835,3 +841,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = alarm_control_panel.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 917f046551d98..5055550be7ce7 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -38,6 +38,7 @@ 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_unique_id, @@ -274,6 +275,10 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): state = hass.states.get("binary_sensor.test") assert state.state == STATE_OFF + async_fire_mqtt_message(hass, "test-topic", "None") + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNKNOWN + async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): """Test the setting of the value via MQTT.""" @@ -864,7 +869,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG, None ) @@ -875,8 +880,19 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[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")], +) async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer + hass, mqtt_mock, caplog, tmp_path, freezer, payload1, state1, payload2, state2 ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN @@ -897,13 +913,13 @@ async def test_cleanup_triggers_and_restoring_state( {binary_sensor.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() - async_fire_mqtt_message(hass, "test-topic1", "ON") + async_fire_mqtt_message(hass, "test-topic1", payload1) state = hass.states.get("binary_sensor.test1") - assert state.state == "on" + assert state.state == state1 - async_fire_mqtt_message(hass, "test-topic2", "ON") + async_fire_mqtt_message(hass, "test-topic2", payload1) state = hass.states.get("binary_sensor.test2") - assert state.state == "on" + assert state.state == state1 freezer.move_to("2022-02-02 12:01:10+01:00") @@ -919,18 +935,18 @@ async def test_cleanup_triggers_and_restoring_state( assert "State recovered after reload for binary_sensor.test2" not in caplog.text state = hass.states.get("binary_sensor.test1") - assert state.state == "on" + assert state.state == state1 state = hass.states.get("binary_sensor.test2") assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "test-topic1", "OFF") + async_fire_mqtt_message(hass, "test-topic1", payload2) state = hass.states.get("binary_sensor.test1") - assert state.state == "off" + assert state.state == state2 - async_fire_mqtt_message(hass, "test-topic2", "OFF") + async_fire_mqtt_message(hass, "test-topic2", payload2) state = hass.states.get("binary_sensor.test2") - assert state.state == "off" + assert state.state == state2 async def test_skip_restoring_state_with_over_due_expire_trigger( diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index a533e0f0ec717..83ef7a42705cc 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -18,6 +18,7 @@ help_test_discovery_update, help_test_discovery_update_attr, help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -25,6 +26,7 @@ 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, @@ -302,6 +304,19 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): ) +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock, + button.DOMAIN, + DEFAULT_CONFIG, + button.SERVICE_PRESS, + command_payload="PRESS", + state_topic=None, + ) + + async def test_invalid_device_class(hass, mqtt_mock): """Test device_class option with invalid value.""" assert await async_setup_component( @@ -391,3 +406,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = button.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 95e8c467a5298..07fd7dc2c1434 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -27,6 +27,7 @@ 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, @@ -237,7 +238,13 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, "test_topic", b"ON" + hass, + mqtt_mock, + camera.DOMAIN, + DEFAULT_CONFIG, + None, + state_topic="test_topic", + state_payload=b"ON", ) @@ -246,3 +253,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = camera.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 3b2da69f94b83..c3501267e1295 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -58,6 +58,7 @@ 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, @@ -81,9 +82,34 @@ "temperature_high_command_topic": "temperature-high-topic", "fan_mode_command_topic": "fan-mode-topic", "swing_mode_command_topic": "swing-mode-topic", + "aux_command_topic": "aux-topic", + "preset_mode_command_topic": "preset-mode-topic", + "preset_modes": [ + "eco", + "away", + "boost", + "comfort", + "home", + "sleep", + "activity", + ], + } +} + +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +DEFAULT_LEGACY_CONFIG = { + CLIMATE_DOMAIN: { + "platform": "mqtt", + "name": "test", + "mode_command_topic": "mode-topic", + "temperature_command_topic": "temperature-topic", + "temperature_low_command_topic": "temperature-low-topic", + "temperature_high_command_topic": "temperature-high-topic", + "fan_mode_command_topic": "fan-mode-topic", + "swing_mode_command_topic": "swing-mode-topic", + "aux_command_topic": "aux-topic", "away_mode_command_topic": "away-mode-topic", "hold_command_topic": "hold-topic", - "aux_command_topic": "aux-topic", } } @@ -102,6 +128,42 @@ async def test_setup_params(hass, mqtt_mock): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP +async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): + """Test the preset mode payload reset configuration.""" + config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config["preset_modes"].append("none") + assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) + await hass.async_block_till_done() + assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text + state = hass.states.get(ENTITY_CLIMATE) + assert state is None + + +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "parameter,config_value", + [ + ("away_mode_command_topic", "away-mode-command-topic"), + ("away_mode_state_topic", "away-mode-state-topic"), + ("away_mode_state_template", "{{ value_json }}"), + ("hold_mode_command_topic", "hold-mode-command-topic"), + ("hold_mode_command_template", "hold-mode-command-template"), + ("hold_mode_state_topic", "hold-mode-state-topic"), + ("hold_mode_state_template", "{{ value_json }}"), + ], +) +async def test_preset_modes_deprecation_guard( + hass, mqtt_mock, caplog, parameter, config_value +): + """Test the configuration for invalid legacy parameters.""" + config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config[parameter] = config_value + assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state is None + + async def test_supported_features(hass, mqtt_mock): """Test the supported_features.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -468,9 +530,99 @@ async def test_handle_action_received(hass, mqtt_mock): assert hvac_action == action +async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): + """Test setting of the preset mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "away", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + + await common.async_set_preset_mode(hass, "eco", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "eco" + + await common.async_set_preset_mode(hass, "none", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "none", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + await common.async_set_preset_mode(hass, "comfort", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "comfort", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "comfort" + + await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) + assert "'invalid' is not a valid preset mode" in caplog.text + + +async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): + """Test setting of the preset mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["climate"]["preset_mode_state_topic"] = "preset-mode-state" + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + async_fire_mqtt_message(hass, "preset-mode-state", "away") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + + async_fire_mqtt_message(hass, "preset-mode-state", "eco") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "eco" + + async_fire_mqtt_message(hass, "preset-mode-state", "none") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + async_fire_mqtt_message(hass, "preset-mode-state", "comfort") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "comfort" + + async_fire_mqtt_message(hass, "preset-mode-state", "None") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + async_fire_mqtt_message(hass, "preset-mode-state", "home") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "home" + + async_fire_mqtt_message(hass, "preset-mode-state", "nonsense") + assert ( + "'nonsense' received on topic preset-mode-state. 'nonsense' is not a valid preset mode" + in caplog.text + ) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "home" + + +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_away_mode_pessimistic(hass, mqtt_mock): """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() @@ -495,9 +647,10 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "none" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_away_mode(hass, mqtt_mock): """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["payload_on"] = "AN" config["climate"]["payload_off"] = "AUS" @@ -536,9 +689,10 @@ async def test_set_away_mode(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "away" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_hold_pessimistic(hass, mqtt_mock): """Test setting the hold mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() @@ -559,9 +713,10 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "none" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_hold(hass, mqtt_mock): """Test setting the hold mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) @@ -590,9 +745,10 @@ async def test_set_hold(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "none" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_preset_away(hass, mqtt_mock): """Test setting the hold mode and away mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) @@ -623,9 +779,10 @@ async def test_set_preset_away(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "hold-on-again" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_preset_away_pessimistic(hass, mqtt_mock): """Test setting the hold mode and away mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) @@ -673,9 +830,10 @@ async def test_set_preset_away_pessimistic(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "hold-on-again" +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 async def test_set_preset_mode_twice(hass, mqtt_mock): """Test setting of the same mode twice only publishes once.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) @@ -803,21 +961,19 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): # By default, just unquote the JSON-strings config["climate"]["value_template"] = "{{ value_json }}" config["climate"]["action_template"] = "{{ value_json }}" - # Something more complicated for hold mode - config["climate"]["hold_state_template"] = "{{ value_json.attribute }}" # Rendering to a bool for aux heat config["climate"]["aux_state_template"] = "{{ value == 'switchmeon' }}" + # Rendering preset_mode + config["climate"]["preset_mode_value_template"] = "{{ value_json.attribute }}" config["climate"]["action_topic"] = "action" config["climate"]["mode_state_topic"] = "mode-state" config["climate"]["fan_mode_state_topic"] = "fan-state" config["climate"]["swing_mode_state_topic"] = "swing-state" config["climate"]["temperature_state_topic"] = "temperature-state" - config["climate"]["away_mode_state_topic"] = "away-state" - config["climate"]["hold_state_topic"] = "hold-state" config["climate"]["aux_state_topic"] = "aux-state" config["climate"]["current_temperature_topic"] = "current-temperature" - + config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() @@ -853,31 +1009,18 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): # ... but the actual value stays unchanged. assert state.attributes.get("temperature") == 1031 - # Away Mode + # Preset Mode assert state.attributes.get("preset_mode") == "none" - async_fire_mqtt_message(hass, "away-state", '"ON"') - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Away Mode with JSON values - async_fire_mqtt_message(hass, "away-state", "false") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "true") + async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}') state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Hold Mode + assert state.attributes.get("preset_mode") == "eco" + # Test with an empty json async_fire_mqtt_message( - hass, - "hold-state", - """ - { "attribute": "somemode" } - """, + hass, "current-preset-mode", '{"other_attribute": "some_value"}' ) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "somemode" + assert "Ignoring empty preset_mode from 'current-preset-mode'" + assert state.attributes.get("preset_mode") == "eco" # Aux mode assert state.attributes.get("aux_heat") == "off" @@ -910,12 +1053,60 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): ) -async def test_set_with_templates(hass, mqtt_mock, caplog): +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog): + """Test getting various for hold and away mode attributes with templates.""" + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) + config["climate"]["mode_state_topic"] = "mode-state" + # By default, just unquote the JSON-strings + config["climate"]["value_template"] = "{{ value_json }}" + # Something more complicated for hold mode + config["climate"]["hold_state_template"] = "{{ value_json.attribute }}" + config["climate"]["away_mode_state_topic"] = "away-state" + config["climate"]["hold_state_topic"] = "hold-state" + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + # Operation Mode + state = hass.states.get(ENTITY_CLIMATE) + async_fire_mqtt_message(hass, "mode-state", '"cool"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "cool" + + # Away Mode + assert state.attributes.get("preset_mode") == "none" + async_fire_mqtt_message(hass, "away-state", '"ON"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + + # Away Mode with JSON values + async_fire_mqtt_message(hass, "away-state", "false") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + async_fire_mqtt_message(hass, "away-state", "true") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + + # Hold Mode + async_fire_mqtt_message( + hass, + "hold-state", + """ + { "attribute": "somemode" } + """, + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "somemode" + + +async def test_set_and_templates(hass, mqtt_mock, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # Create simple templates config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}" - config["climate"]["hold_command_template"] = "hold: {{ value }}" + config["climate"]["preset_mode_command_template"] = "preset_mode: {{ value }}" config["climate"]["mode_command_template"] = "mode: {{ value }}" config["climate"]["swing_mode_command_template"] = "swing_mode: {{ value }}" config["climate"]["temperature_command_template"] = "temp: {{ value }}" @@ -934,11 +1125,12 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "high" - # Hold Mode + # Preset Mode await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False) + mqtt_mock.async_publish.call_count == 1 + mqtt_mock.async_publish.assert_any_call( + "preset-mode-topic", "preset_mode: eco", 0, False + ) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_ECO @@ -986,6 +1178,26 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): assert state.attributes.get("target_temp_high") == 23 +# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplog): + """Test setting various attributes on hold and away mode with templates.""" + config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) + # Create simple templates + config["climate"]["hold_command_template"] = "hold: {{ value }}" + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + # Hold Mode + await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) + mqtt_mock.async_publish.call_count == 2 + mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) + mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == PRESET_ECO + + async def test_min_temp_custom(hass, mqtt_mock): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1117,9 +1329,11 @@ async def test_unique_id(hass, mqtt_mock): ("action_topic", "heating", ATTR_HVAC_ACTION, "heating"), ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 ("away_mode_state_topic", "ON", ATTR_PRESET_MODE, "away"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 ("hold_state_topic", "mode1", ATTR_PRESET_MODE, "mode1"), ("mode_state_topic", "cool", None, None), ("mode_state_topic", "fan_only", None, None), @@ -1134,7 +1348,11 @@ async def test_encoding_subscribable_topics( ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - config["hold_modes"] = ["mode1", "mode2"] + # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 + if topic in ["hold_state_topic", "away_mode_state_topic"]: + config["hold_modes"] = ["mode1", "mode2"] + del config["preset_modes"] + del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, mqtt_mock, @@ -1240,11 +1458,19 @@ async def test_entity_debug_info_message(hass, mqtt_mock): CLIMATE_DOMAIN: { "platform": "mqtt", "name": "test", + "mode_command_topic": "command-topic", "mode_state_topic": "test-topic", } } await help_test_entity_debug_info_message( - hass, mqtt_mock, CLIMATE_DOMAIN, config, "test-topic" + hass, + mqtt_mock, + CLIMATE_DOMAIN, + config, + climate.SERVICE_TURN_ON, + command_topic="command-topic", + command_payload="heat", + state_topic="test-topic", ) @@ -1308,6 +1534,13 @@ async def test_precision_whole(hass, mqtt_mock): "cool", "mode_command_template", ), + ( + climate.SERVICE_SET_PRESET_MODE, + "preset_mode_command_topic", + {"preset_mode": "sleep"}, + "sleep", + "preset_mode_command_template", + ), ( climate.SERVICE_SET_PRESET_MODE, "away_mode_command_topic", @@ -1325,8 +1558,8 @@ async def test_precision_whole(hass, mqtt_mock): ( climate.SERVICE_SET_PRESET_MODE, "hold_command_topic", - {"preset_mode": "some_hold_mode"}, - "some_hold_mode", + {"preset_mode": "comfort"}, + "comfort", "hold_command_template", ), ( @@ -1393,7 +1626,10 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG[domain] + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + if topic != "preset_mode_command_topic": + del config["preset_mode_command_topic"] + del config["preset_modes"] await help_test_publishing_with_custom_encoding( hass, @@ -1414,3 +1650,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = CLIMATE_DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 78c37b1105a4c..8cf7353d196fc 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -21,7 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from tests.common import async_fire_mqtt_message, mock_registry +from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_registry DEFAULT_CONFIG_DEVICE_INFO_ID = { "identifiers": ["helloworld"], @@ -43,6 +43,8 @@ "configuration_url": "http://example.com", } +_SENTINEL = object() + async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, config): """Test availability after MQTT disconnection.""" @@ -1110,7 +1112,7 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 1 assert ( debug_info_data["entities"][0]["discovery_data"]["topic"] @@ -1121,6 +1123,7 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ "subscriptions" ] + assert debug_info_data["entities"][0]["transmitted"] == [] assert len(debug_info_data["triggers"]) == 0 @@ -1143,7 +1146,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1155,7 +1158,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf for i in range(0, debug_info.STORED_MESSAGES + 1): async_fire_mqtt_message(hass, "test-topic", f"{i}") - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert ( len(debug_info_data["entities"][0]["subscriptions"][0]["messages"]) @@ -1177,9 +1180,18 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async def help_test_entity_debug_info_message( - hass, mqtt_mock, domain, config, topic=None, payload=None + hass, + mqtt_mock, + domain, + config, + service, + command_topic=_SENTINEL, + command_payload=_SENTINEL, + state_topic=_SENTINEL, + state_payload=_SENTINEL, + service_parameters=None, ): - """Test debug_info message overflow. + """Test debug_info. This is a test helper for MQTT debug_info. """ @@ -1188,13 +1200,21 @@ async def help_test_entity_debug_info_message( config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" - if topic is None: + if command_topic is _SENTINEL: + # Add default topic to config + config["command_topic"] = "command-topic" + command_topic = "command-topic" + + if command_payload is _SENTINEL: + command_payload = "ON" + + if state_topic is _SENTINEL: # Add default topic to config config["state_topic"] = "state-topic" - topic = "state-topic" + state_topic = "state-topic" - if payload is None: - payload = "ON" + if state_payload is _SENTINEL: + state_payload = "ON" registry = dr.async_get(hass) @@ -1205,31 +1225,69 @@ async def help_test_entity_debug_info_message( device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) - assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 - assert {"topic": topic, "messages": []} in debug_info_data["entities"][0][ - "subscriptions" - ] + debug_info_data = debug_info.info_for_device(hass, device.id) start_dt = datetime(2019, 1, 1, 0, 0, 0) - with patch("homeassistant.util.dt.utcnow") as dt_utcnow: - dt_utcnow.return_value = start_dt - async_fire_mqtt_message(hass, topic, payload) - debug_info_data = await debug_info.info_for_device(hass, device.id) - assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 - assert { - "topic": topic, - "messages": [ + if state_topic is not None: + assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 + assert {"topic": state_topic, "messages": []} in debug_info_data["entities"][0][ + "subscriptions" + ] + + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + async_fire_mqtt_message(hass, state_topic, state_payload) + + debug_info_data = debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 + assert { + "topic": state_topic, + "messages": [ + { + "payload": str(state_payload), + "qos": 0, + "retain": False, + "time": start_dt, + "topic": state_topic, + } + ], + } in debug_info_data["entities"][0]["subscriptions"] + + expected_transmissions = [] + if service: + # Trigger an outgoing MQTT message + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + if service: + service_data = {ATTR_ENTITY_ID: f"{domain}.test"} + if service_parameters: + service_data.update(service_parameters) + + await hass.services.async_call( + domain, + service, + service_data, + blocking=True, + ) + + expected_transmissions = [ { - "payload": str(payload), - "qos": 0, - "retain": False, - "time": start_dt, - "topic": topic, + "topic": command_topic, + "messages": [ + { + "payload": str(command_payload), + "qos": 0, + "retain": False, + "time": start_dt, + "topic": command_topic, + } + ], } - ], - } in debug_info_data["entities"][0]["subscriptions"] + ] + + debug_info_data = debug_info.info_for_device(hass, device.id) + assert debug_info_data["entities"][0]["transmitted"] == expected_transmissions async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): @@ -1251,7 +1309,7 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 1 assert ( debug_info_data["entities"][0]["discovery_data"]["topic"] @@ -1269,7 +1327,7 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") await hass.async_block_till_done() - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 0 assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] @@ -1295,7 +1353,7 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 1 assert ( debug_info_data["entities"][0]["discovery_data"]["topic"] @@ -1313,7 +1371,7 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, await hass.async_block_till_done() await hass.async_block_till_done() - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 1 assert ( debug_info_data["entities"][0]["discovery_data"]["topic"] @@ -1561,7 +1619,61 @@ async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config assert hass.states.get(f"{domain}.test_old_2") assert len(hass.states.async_all(domain)) == 2 - # Create temporary fixture for configuration.yaml based on the supplied config and test a reload with this new config + # 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_3 = copy.deepcopy(config) + new_config_3["name"] = "test_new_3" + + await help_test_reload_with_config( + hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] + ) + + assert len(hass.states.async_all(domain)) == 3 + + 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") + + +async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): + """Test reloading an MQTT platform when config entry is setup late.""" + # Create and test an old config of 2 entities based on the config supplied + 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 + # 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) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index befdc139eeba7..d9aab02e821fe 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,5 +1,4 @@ """Test config flow.""" - from unittest.mock import patch import pytest @@ -30,6 +29,31 @@ def mock_try_connection(): yield mock_try +@pytest.fixture +def mock_try_connection_success(): + """Mock the try connection method with success.""" + + def loop_start(): + """Simulate connect on loop start.""" + mock_client().on_connect(mock_client, None, None, 0) + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().loop_start = loop_start + yield mock_client() + + +@pytest.fixture +def mock_try_connection_time_out(): + """Mock the try connection method with a time out.""" + + # Patch prevent waiting 5 sec for a timeout + with patch("paho.mqtt.client.Client") as mock_client, patch( + "homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0 + ): + mock_client().loop_start = lambda *args: 1 + yield mock_client() + + async def test_user_connection_works( hass, mock_try_connection, mock_finish_setup, mqtt_client_mock ): @@ -57,10 +81,10 @@ async def test_user_connection_works( assert len(mock_finish_setup.mock_calls) == 1 -async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setup): +async def test_user_connection_fails( + hass, mock_try_connection_time_out, mock_finish_setup +): """Test if connection cannot be made.""" - mock_try_connection.return_value = False - result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) @@ -74,25 +98,62 @@ async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setu assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection - assert len(mock_try_connection.mock_calls) == 1 + assert len(mock_try_connection_time_out.mock_calls) # Check config entry did not setup assert len(mock_finish_setup.mock_calls) == 0 +async def test_manual_config_starts_discovery_flow( + hass, mock_try_connection, mock_finish_setup, mqtt_client_mock +): + """Test manual config initiates a discovery flow.""" + # No flows in progress + assert hass.config_entries.flow.async_progress() == [] + + # MQTT config present in yaml config + assert await async_setup_component(hass, "mqtt", {"mqtt": {}}) + await hass.async_block_till_done() + assert len(mock_finish_setup.mock_calls) == 0 + + # There should now be a discovery flow + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "integration_discovery" + assert flows[0]["handler"] == "mqtt" + assert flows[0]["step_id"] == "broker" + + async def test_manual_config_set( hass, mock_try_connection, mock_finish_setup, mqtt_client_mock ): - """Test we ignore entry if manual config available.""" + """Test manual config does not create an entry, and entry can be setup late.""" + # MQTT config present in yaml config assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() - assert len(mock_finish_setup.mock_calls) == 1 + assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True + # Start config flow result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"broker": "127.0.0.1"} + ) + + assert result["type"] == "create_entry" + assert result["result"].data == { + "broker": "127.0.0.1", + "port": 1883, + "discovery": True, + } + # Check we tried the connection, with precedence for config entry settings + mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None) + # Check config entry got setup + assert len(mock_finish_setup.mock_calls) == 1 async def test_user_single_instance(hass): @@ -126,7 +187,12 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( mqtt.DOMAIN, data=HassioServiceInfo( - config={"addon": "Mosquitto", "host": "mock-mosquitto", "port": "1883"} + config={ + "addon": "Mosquitto", + "host": "mock-mosquitto", + "port": "1883", + "protocol": "3.1.1", + } ), context={"source": config_entries.SOURCE_HASSIO}, ) @@ -135,9 +201,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: assert result.get("reason") == "already_configured" -async def test_hassio_confirm( - hass, mock_try_connection, mock_finish_setup, mqtt_client_mock -): +async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_setup): """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -159,6 +223,7 @@ async def test_hassio_confirm( assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} + mock_try_connection_success.reset_mock() result = await hass.config_entries.flow.async_configure( result["flow_id"], {"discovery": True} ) @@ -173,7 +238,7 @@ async def test_hassio_confirm( "discovery": True, } # Check we tried the connection - assert len(mock_try_connection.mock_calls) == 1 + assert len(mock_try_connection_success.mock_calls) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 @@ -331,10 +396,9 @@ def get_suggested(schema, key): async def test_option_flow_default_suggested_values( - hass, mqtt_mock, mock_try_connection + hass, mqtt_mock, mock_try_connection_success ): """Test config flow options has default/suggested values.""" - mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { mqtt.CONF_BROKER: "test-broker", @@ -479,7 +543,7 @@ async def test_option_flow_default_suggested_values( await hass.async_block_till_done() -async def test_options_user_connection_fails(hass, mock_try_connection): +async def test_options_user_connection_fails(hass, mock_try_connection_time_out): """Test if connection cannot be made.""" config_entry = MockConfigEntry(domain=mqtt.DOMAIN) config_entry.add_to_hass(hass) @@ -487,12 +551,10 @@ async def test_options_user_connection_fails(hass, mock_try_connection): mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PORT: 1234, } - - mock_try_connection.return_value = False - result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" + mock_try_connection_time_out.reset_mock() result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "bad-broker", mqtt.CONF_PORT: 2345}, @@ -502,7 +564,7 @@ async def test_options_user_connection_fails(hass, mock_try_connection): assert result["errors"]["base"] == "cannot_connect" # Check we tried the connection - assert len(mock_try_connection.mock_calls) == 1 + assert len(mock_try_connection_time_out.mock_calls) # Check config entry did not update assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 0d24f805cc12e..aad6fa5d9ca32 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -64,6 +64,7 @@ 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, @@ -2521,7 +2522,12 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock, + cover.DOMAIN, + DEFAULT_CONFIG, + SERVICE_OPEN_COVER, + command_payload="OPEN", ) @@ -3168,6 +3174,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 4020c2beaebdb..3b83581b86aa5 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -5,6 +5,7 @@ from homeassistant.components import device_tracker from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN +from homeassistant.setup import async_setup_component from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message @@ -183,8 +184,13 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): assert state.name == "Cider" -async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_tracker( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock +): """Test discvered device is cleaned up when removed from registry.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -203,7 +209,16 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): state = hass.states.get("device_tracker.mqtt_unique") assert state is not None - device_reg.async_remove_device(device_entry.id) + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "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() diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index a5359563d926f..8a3719f1707b8 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -646,9 +646,12 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, device_reg, calls, mqtt_mock + hass, hass_ws_client, device_reg, calls, mqtt_mock ): """Test triggers not firing after removal.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -688,8 +691,16 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( await hass.async_block_till_done() assert len(calls) == 1 - # Remove the device - device_reg.async_remove_device(device_entry.id) + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + assert response["success"] await hass.async_block_till_done() async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") @@ -967,8 +978,11 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): """Test trigger discovery topic is cleaned when device is removed from registry.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + config = { "automation_type": "trigger", "topic": "test-topic", @@ -990,7 +1004,16 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): ) assert triggers[0]["type"] == "foo" - device_reg.async_remove_device(device_entry.id) + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "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() @@ -1246,7 +1269,7 @@ async def test_trigger_debug_info(hass, mqtt_mock): ) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 2 topic_map = { @@ -1268,7 +1291,7 @@ async def test_trigger_debug_info(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") await hass.async_block_till_done() - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 1 assert ( diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py new file mode 100644 index 0000000000000..bbd42a20c87be --- /dev/null +++ b/tests/components/mqtt/test_diagnostics.py @@ -0,0 +1,263 @@ +"""Test MQTT diagnostics.""" + +import json +from unittest.mock import ANY + +import pytest + +from homeassistant.components import mqtt + +from tests.common import async_fire_mqtt_message, mock_device_registry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + +default_config = { + "birth_message": {}, + "broker": "mock-broker", + "discovery": True, + "discovery_prefix": "homeassistant", + "keepalive": 60, + "port": 1883, + "protocol": "3.1.1", + "tls_version": "auto", + "will_message": { + "payload": "offline", + "qos": 0, + "retain": False, + "topic": "homeassistant/status", + }, +} + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): + """Test config entry diagnostics.""" + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + mqtt_mock.connected = True + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "connected": True, + "devices": [], + "mqtt_config": default_config, + "mqtt_debug_info": {"entities": [], "triggers": []}, + } + + # Discover a device with an entity and a trigger + config_sensor = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + config_trigger = { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "topic": "test-topic1", + "type": "foo", + "subtype": "bar", + } + data_sensor = json.dumps(config_sensor) + data_trigger = json.dumps(config_trigger) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor) + async_fire_mqtt_message( + hass, "homeassistant/device_automation/bla/config", data_trigger + ) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + expected_debug_info = { + "entities": [ + { + "entity_id": "sensor.mqtt_sensor", + "subscriptions": [{"topic": "foobar/sensor", "messages": []}], + "discovery_data": { + "payload": config_sensor, + "topic": "homeassistant/sensor/bla/config", + }, + "transmitted": [], + } + ], + "triggers": [ + { + "discovery_data": { + "payload": config_trigger, + "topic": "homeassistant/device_automation/bla/config", + }, + "trigger_key": ["device_automation", "bla"], + } + ], + } + + expected_device = { + "disabled": False, + "disabled_by": None, + "entities": [ + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "entity_id": "sensor.mqtt_sensor", + "icon": None, + "original_device_class": None, + "original_icon": None, + "state": { + "attributes": {"friendly_name": "MQTT Sensor"}, + "entity_id": "sensor.mqtt_sensor", + "last_changed": ANY, + "last_updated": ANY, + "state": "unknown", + }, + "unit_of_measurement": None, + } + ], + "id": device_entry.id, + "name": None, + "name_by_user": None, + } + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "connected": True, + "devices": [expected_device], + "mqtt_config": default_config, + "mqtt_debug_info": expected_debug_info, + } + + assert await get_diagnostics_for_device( + hass, hass_client, config_entry, device_entry + ) == { + "connected": True, + "device": expected_device, + "mqtt_config": default_config, + "mqtt_debug_info": expected_debug_info, + } + + +@pytest.mark.parametrize( + "mqtt_config", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + mqtt.CONF_PASSWORD: "hunter2", + mqtt.CONF_USERNAME: "my_user", + } + ], +) +async def test_redact_diagnostics(hass, device_reg, hass_client, mqtt_mock): + """Test redacting diagnostics.""" + expected_config = dict(default_config) + expected_config["password"] = "**REDACTED**" + expected_config["username"] = "**REDACTED**" + + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + mqtt_mock.connected = True + + # Discover a device with a device tracker + config_tracker = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "state_topic": "foobar/device_tracker", + "json_attributes_topic": "attributes-topic", + "unique_id": "unique", + } + data_tracker = json.dumps(config_tracker) + + async_fire_mqtt_message( + hass, "homeassistant/device_tracker/bla/config", data_tracker + ) + await hass.async_block_till_done() + + location_data = '{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}' + async_fire_mqtt_message(hass, "attributes-topic", location_data) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + expected_debug_info = { + "entities": [ + { + "entity_id": "device_tracker.mqtt_unique", + "subscriptions": [ + { + "topic": "attributes-topic", + "messages": [ + { + "payload": location_data, + "qos": 0, + "retain": False, + "time": ANY, + "topic": "attributes-topic", + } + ], + }, + {"topic": "foobar/device_tracker", "messages": []}, + ], + "discovery_data": { + "payload": config_tracker, + "topic": "homeassistant/device_tracker/bla/config", + }, + "transmitted": [], + } + ], + "triggers": [], + } + + expected_device = { + "disabled": False, + "disabled_by": None, + "entities": [ + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "entity_id": "device_tracker.mqtt_unique", + "icon": None, + "original_device_class": None, + "original_icon": None, + "state": { + "attributes": { + "gps_accuracy": 1.5, + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "source_type": None, + }, + "entity_id": "device_tracker.mqtt_unique", + "last_changed": ANY, + "last_updated": ANY, + "state": "home", + }, + "unit_of_measurement": None, + } + ], + "id": device_entry.id, + "name": None, + "name_by_user": None, + } + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "connected": True, + "devices": [expected_device], + "mqtt_config": expected_config, + "mqtt_debug_info": expected_debug_info, + } + + assert await get_diagnostics_for_device( + hass, hass_client, config_entry, device_entry + ) == { + "connected": True, + "device": expected_device, + "mqtt_config": expected_config, + "mqtt_debug_info": expected_debug_info, + } diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index de9150de1a223..463f3d03fff10 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,8 @@ """The tests for the MQTT discovery.""" +import json from pathlib import Path import re -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, call, patch import pytest @@ -14,13 +15,15 @@ from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import ( EVENT_STATE_CHANGED, - STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) import homeassistant.core as ha +from homeassistant.setup import async_setup_component from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, mock_device_registry, mock_entity_platform, @@ -565,8 +568,11 @@ async def test_duplicate_removal(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): - """Test discvered device is cleaned up when removed from registry.""" +async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): + """Test discvered device is cleaned up when entry removed from device.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -585,7 +591,16 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): state = hass.states.get("sensor.mqtt_sensor") assert state is not None - device_reg.async_remove_device(device_entry.id) + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "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() @@ -606,6 +621,215 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): ) +async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): + """Test discvered device is cleaned up when removed through MQTT.""" + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + 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("sensor.mqtt_sensor") + assert entity_entry is not None + + state = hass.states.get("sensor.mqtt_sensor") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "") + 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("sensor.mqtt_sensor") + assert entity_entry is None + + # Verify state is removed + state = hass.states.get("sensor.mqtt_sensor") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topics have not been cleared again + mqtt_mock.async_publish.assert_not_called() + + +async def test_cleanup_device_multiple_config_entries( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock +): + """Test discovered device is cleaned up when entry removed from device.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + 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={("mac", "12:34:56:AB:CD:EF")}, + ) + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + sensor_config = { + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + tag_config = { + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + "topic": "test-topic", + } + trigger_config = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + } + + sensor_data = json.dumps(sensor_config) + tag_data = json.dumps(tag_config) + trigger_data = json.dumps(trigger_config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", sensor_data) + async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", tag_data) + async_fire_mqtt_message( + hass, "homeassistant/device_automation/bla/config", trigger_data + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + assert device_entry.config_entries == { + mqtt_config_entry.entry_id, + config_entry.entry_id, + } + entity_entry = entity_reg.async_get("sensor.mqtt_sensor") + assert entity_entry is not None + + state = hass.states.get("sensor.mqtt_sensor") + assert state is not None + + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "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 is still there but entity is cleared + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + entity_entry = entity_reg.async_get("sensor.mqtt_sensor") + assert device_entry.config_entries == {config_entry.entry_id} + assert entity_entry is None + + # Verify state is removed + state = hass.states.get("sensor.mqtt_sensor") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topic has been cleared + mqtt_mock.async_publish.assert_has_calls( + [ + call("homeassistant/sensor/bla/config", "", 0, True), + call("homeassistant/tag/bla/config", "", 0, True), + call("homeassistant/device_automation/bla/config", "", 0, True), + ], + any_order=True, + ) + + +async def test_cleanup_device_multiple_config_entries_mqtt( + hass, device_reg, entity_reg, mqtt_mock +): + """Test discovered device is cleaned up when removed through MQTT.""" + 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={("mac", "12:34:56:AB:CD:EF")}, + ) + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + sensor_config = { + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + tag_config = { + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + "topic": "test-topic", + } + trigger_config = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"connections": [["mac", "12:34:56:AB:CD:EF"]]}, + } + + sensor_data = json.dumps(sensor_config) + tag_data = json.dumps(tag_config) + trigger_data = json.dumps(trigger_config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", sensor_data) + async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", tag_data) + async_fire_mqtt_message( + hass, "homeassistant/device_automation/bla/config", trigger_data + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + assert device_entry.config_entries == { + mqtt_config_entry.entry_id, + config_entry.entry_id, + } + entity_entry = entity_reg.async_get("sensor.mqtt_sensor") + assert entity_entry is not None + + state = hass.states.get("sensor.mqtt_sensor") + assert state is not None + + # Send MQTT messages to remove + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "") + async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "") + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") + + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify device is still there but entity is cleared + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + entity_entry = entity_reg.async_get("sensor.mqtt_sensor") + assert device_entry.config_entries == {config_entry.entry_id} + assert entity_entry is None + + # Verify state is removed + state = hass.states.get("sensor.mqtt_sensor") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topics have not been cleared again + mqtt_mock.async_publish.assert_not_called() + + async def test_discovery_expansion(hass, mqtt_mock, caplog): """Test expansion of abbreviated discovery payload.""" data = ( @@ -649,7 +873,7 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "test_topic/some/base/topic", "ON") @@ -699,7 +923,7 @@ async def test_discovery_expansion_2(hass, mqtt_mock, caplog): assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN @pytest.mark.no_fail_on_log_exception @@ -773,7 +997,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") @@ -819,7 +1043,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_2( assert state is not None assert state.name == "DiscoveryExpansionTest1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") @@ -895,13 +1119,13 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): assert state is not None assert state.name == "Test1" assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == "off" + assert state.state == STATE_UNKNOWN assert state.attributes["assumed_state"] is True async_fire_mqtt_message(hass, "homeassistant/switch/bla/state", "ON") state = hass.states.get("switch.Test1") - assert state.state == "off" + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0a3b2499dc2c5..5418727ec0ed3 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -27,6 +27,7 @@ ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component @@ -50,6 +51,7 @@ 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, @@ -119,7 +121,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "StAtE_On") @@ -194,6 +196,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_PERCENTAGE) is None assert state.attributes.get(fan.ATTR_SPEED) is None + async_fire_mqtt_message(hass, "state-topic", "None") + state = hass.states.get("fan.test") + assert state.state == STATE_UNKNOWN + async def test_controlling_state_via_topic_with_different_speed_range( hass, mqtt_mock, caplog @@ -285,7 +291,7 @@ async def test_controlling_state_via_topic_no_percentage_topics( await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "preset-mode-state-topic", "smart") @@ -349,13 +355,17 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", '{"val":"ON"}') state = hass.states.get("fan.test") assert state.state == STATE_ON + async_fire_mqtt_message(hass, "state-topic", '{"val": null}') + state = hass.states.get("fan.test") + assert state.state == STATE_UNKNOWN + async_fire_mqtt_message(hass, "state-topic", '{"val":"OFF"}') state = hass.states.get("fan.test") assert state.state == STATE_OFF @@ -449,7 +459,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message( @@ -527,7 +537,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test") @@ -748,7 +758,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test") @@ -883,7 +893,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test") @@ -1022,7 +1032,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_set_preset_mode(hass, "fan.test", "medium") @@ -1086,7 +1096,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "fan.test") @@ -1344,7 +1354,7 @@ async def test_attributes(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("fan.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "fan.test") state = hass.states.get("fan.test") @@ -1715,7 +1725,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, fan.SERVICE_TURN_ON ) @@ -1794,3 +1804,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 62d29c12ee82b..4aa5ff2350b90 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -28,6 +28,7 @@ SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component @@ -51,6 +52,7 @@ 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, @@ -157,7 +159,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "StAtE_On") @@ -220,6 +222,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("humidifier.test") assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None + async_fire_mqtt_message(hass, "state-topic", "None") + state = hass.states.get("humidifier.test") + assert state.state == STATE_UNKNOWN + async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): """Test the controlling state via topic and JSON message.""" @@ -250,7 +256,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", '{"val":"ON"}') @@ -301,6 +307,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap assert "Ignoring empty mode from" in caplog.text caplog.clear() + async_fire_mqtt_message(hass, "state-topic", '{"val": null}') + state = hass.states.get("humidifier.test") + assert state.state == STATE_UNKNOWN + async def test_controlling_state_via_topic_and_json_message_shared_topic( hass, mqtt_mock, caplog @@ -333,7 +343,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message( @@ -404,7 +414,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await async_turn_on(hass, "humidifier.test") @@ -498,7 +508,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await async_turn_on(hass, "humidifier.test") @@ -593,7 +603,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) await async_turn_on(hass, "humidifier.test") @@ -731,7 +741,7 @@ async def test_attributes(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("humidifier.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(humidifier.ATTR_AVAILABLE_MODES) == [ "eco", "baby", @@ -1093,7 +1103,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON ) @@ -1165,3 +1175,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = humidifier.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1e11560cfc80e..7296d4e81012f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,16 +1,21 @@ """The tests for the MQTT component.""" import asyncio from datetime import datetime, timedelta +from functools import partial import json +import logging import ssl from unittest.mock import ANY, AsyncMock, MagicMock, call, mock_open, patch import pytest import voluptuous as vol +import yaml +from homeassistant import config as hass_config from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA +from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, @@ -34,6 +39,14 @@ ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES +_LOGGER = logging.getLogger(__name__) + + +class RecordCallsPartial(partial): + """Wrapper class for partial.""" + + __name__ = "RecordCallPartialTest" + @pytest.fixture(autouse=True) def mock_storage(hass_storage): @@ -675,6 +688,10 @@ async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): await hass.async_block_till_done() assert len(calls) == 1 + # Cannot unsubscribe twice + with pytest.raises(HomeAssistantError): + unsub() + async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): """Test the subscription of a topic using the non-async function.""" @@ -706,13 +723,13 @@ async def test_subscribe_bad_topic(hass, mqtt_mock, calls, record_calls): async def test_subscribe_deprecated(hass, mqtt_mock): """Test the subscription of a topic using deprecated callback signature.""" - calls = [] @callback def record_calls(topic, payload, qos): """Record calls.""" calls.append((topic, payload, qos)) + calls = [] unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -728,17 +745,59 @@ def record_calls(topic, payload, qos): await hass.async_block_till_done() assert len(calls) == 1 + mqtt_mock.async_publish.reset_mock() + + # Test with partial wrapper + calls = [] + unsub = await mqtt.async_subscribe( + hass, "test-topic", RecordCallsPartial(record_calls) + ) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0][0] == "test-topic" + assert calls[0][1] == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(calls) == 1 async def test_subscribe_deprecated_async(hass, mqtt_mock): - """Test the subscription of a topic using deprecated callback signature.""" - calls = [] + """Test the subscription of a topic using deprecated coroutine signature.""" - async def record_calls(topic, payload, qos): + def async_record_calls(topic, payload, qos): """Record calls.""" calls.append((topic, payload, qos)) - unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) + calls = [] + unsub = await mqtt.async_subscribe(hass, "test-topic", async_record_calls) + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0][0] == "test-topic" + assert calls[0][1] == "test-payload" + + unsub() + + async_fire_mqtt_message(hass, "test-topic", "test-payload") + + await hass.async_block_till_done() + assert len(calls) == 1 + mqtt_mock.async_publish.reset_mock() + + # Test with partial wrapper + calls = [] + unsub = await mqtt.async_subscribe( + hass, "test-topic", RecordCallsPartial(async_record_calls) + ) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -997,6 +1056,38 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): + """Test not calling unsubscribe() when other subscribers are active.""" + # Fake that the client is connected + mqtt_mock().connected = True + + calls_a = MagicMock() + calls_b = MagicMock() + + mqtt_client_mock.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", calls_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", calls_b) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test/state", "online") + await hass.async_block_till_done() + assert not calls_a.called + assert calls_b.called + + # We allow either calls [subscribe, unsubscribe, subscribe] or [subscribe, subscribe] + expected_calls_1 = [ + call.subscribe("test/state", 0), + call.unsubscribe("test/state"), + call.subscribe("test/state", 0), + ] + expected_calls_2 = [ + call.subscribe("test/state", 0), + call.subscribe("test/state", 0), + ] + assert mqtt_client_mock.mock_calls in (expected_calls_1, expected_calls_2) + + @pytest.mark.parametrize( "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], @@ -1010,9 +1101,9 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 1 - mqtt_mock._mqtt_on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): - mqtt_mock._mqtt_on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -1044,23 +1135,185 @@ async def test_restore_all_active_subscriptions_on_reconnect( await hass.async_block_till_done() assert mqtt_client_mock.unsubscribe.call_count == 0 - mqtt_mock._mqtt_on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0) with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): - mqtt_mock._mqtt_on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() expected.append(call("test/state", 1)) assert mqtt_client_mock.subscribe.mock_calls == expected -async def test_setup_logs_error_if_no_connect_broker(hass, caplog): +async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): + """Test for setup failure if initial client connection fails.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + + mqtt_client_mock.connect.return_value = 1 + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() + assert "Failed to connect to MQTT server:" in caplog.text + + +async def test_logs_error_if_no_connect_broker( + hass, caplog, mqtt_mock, mqtt_client_mock +): """Test for setup failure if connection to broker is missing.""" + # test with rc = 3 -> broker unavailable + mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) + await hass.async_block_till_done() + assert ( + "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." + in caplog.text + ) + + +@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.3) +async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): + """Test receiving an ACK callback before waiting for it.""" + # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 1) + await hass.async_block_till_done() + # Make sure the ACK has been received + await hass.async_block_till_done() + # Now call publish without call back, this will call _wait_for_mid(msg_info.mid) + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + # Since the mid event was already set, we should not see any timeout + await hass.async_block_till_done() + assert ( + "Transmitting message on no_callback/test-topic: 'test-payload', mid: 1" + in caplog.text + ) + assert "No ACK from MQTT server" not in caplog.text + + +async def test_publish_error(hass, caplog): + """Test publish error.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + # simulate an Out of memory error with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 1 + mock_client().publish().rc = 1 assert await mqtt.async_setup_entry(hass, entry) - assert "Failed to connect to MQTT server:" in caplog.text + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", b"test-payload", qos=0, retain=False, encoding=None + ) + assert "Failed to connect to MQTT server: Out of memory." in caplog.text + + +async def test_handle_message_callback(hass, caplog, mqtt_mock, mqtt_client_mock): + """Test for handling an incoming message callback.""" + msg = ReceiveMessage("some-topic", b"test-payload", 0, False) + mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) + await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) + mqtt_client_mock.on_message(mock_mqtt, None, msg) + + await hass.async_block_till_done() + await hass.async_block_till_done() + assert "Received message on some-topic: b'test-payload'" in caplog.text + + +async def test_setup_override_configuration(hass, caplog, tmp_path): + """Test override setup from configuration entry.""" + calls_username_password_set = [] + + def mock_usename_password_set(username, password): + calls_username_password_set.append((username, password)) + + # Mock password setup from config + config = { + "username": "someuser", + "password": "someyamlconfiguredpassword", + "protocol": "3.1", + } + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({mqtt.DOMAIN: config}) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + # Mock config entry + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"}, + ) + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().username_pw_set = mock_usename_password_set + mock_client.on_connect(return_value=0) + await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await entry.async_setup(hass) + await hass.async_block_till_done() + + assert ( + "Data in your configuration entry is going to override your configuration.yaml:" + in caplog.text + ) + + # Check if the protocol was set to 3.1 from configuration.yaml + assert mock_client.call_args[1]["protocol"] == 3 + + # Check if the password override worked + assert calls_username_password_set[0][0] == "someuser" + assert calls_username_password_set[0][1] == "somepassword" + + +async def test_setup_mqtt_client_protocol(hass): + """Test MQTT client protocol setup.""" + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PROTOCOL: "3.1"}, + ) + with patch("paho.mqtt.client.Client") as mock_client: + mock_client.on_connect(return_value=0) + assert await mqtt.async_setup_entry(hass, entry) + + # check if protocol setup was correctly + assert mock_client.call_args[1]["protocol"] == 3 + + +@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.2) +async def test_handle_mqtt_timeout_on_callback(hass, caplog): + """Test publish without receiving an ACK callback.""" + mid = 0 + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch("paho.mqtt.client.Client") as mock_client: + + def _mock_ack(topic, qos=0): + # Handle ACK for subscribe normally + nonlocal mid + mid += 1 + mock_client.on_subscribe(0, 0, mid) + return (0, mid) + + # We want to simulate the publish behaviour MQTT client + mock_client = mock_client.return_value + mock_client.publish.return_value = FakeInfo() + mock_client.subscribe.side_effect = _mock_ack + mock_client.connect.return_value = 0 + + entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + ) + # Set up the integration + assert await mqtt.async_setup_entry(hass, entry) + # Make sure we are connected correctly + mock_client.on_connect(mock_client, None, None, 0) + + # Now call we publish without simulating and ACK callback + await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") + await hass.async_block_till_done() + # The is no ACK so we should see a timeout in the log after publishing + assert len(mock_client.publish.mock_calls) == 1 + assert "No ACK from MQTT server" in caplog.text async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): @@ -1073,18 +1326,29 @@ async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplo assert "Failed to connect to MQTT server due to exception:" in caplog.text -async def test_setup_uses_certificate_on_certificate_set_to_auto(hass): - """Test setup uses bundled certs when certificate is set to auto.""" +@pytest.mark.parametrize("insecure", [None, False, True]) +async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( + hass, insecure +): + """Test setup uses bundled certs when certificate is set to auto and insecure.""" calls = [] + insecure_check = {"insecure": "not set"} def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): calls.append((certificate, certfile, keyfile, tls_version)) + def mock_tls_insecure_set(insecure_param): + insecure_check["insecure"] = insecure_param + + config_item_data = {mqtt.CONF_BROKER: "test-broker", "certificate": "auto"} + if insecure is not None: + config_item_data["tls_insecure"] = insecure with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set + mock_client().tls_insecure_set = mock_tls_insecure_set entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", "certificate": "auto"}, + data=config_item_data, ) assert await mqtt.async_setup_entry(hass, entry) @@ -1097,6 +1361,13 @@ def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): # assert mock_mqtt.mock_calls[0][1][2]["certificate"] == expectedCertificate assert calls[0][0] == expectedCertificate + # test if insecure is set + assert ( + insecure_check["insecure"] == insecure + if insecure is not None + else insecure_check["insecure"] == "not set" + ) + async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): """Test setup defaults to TLSv1 under python3.6.""" @@ -1134,6 +1405,8 @@ def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "birth", mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, }, } ], @@ -1148,7 +1421,7 @@ async def wait_birth(topic, payload, qos): with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "birth", wait_birth) - mqtt_mock._mqtt_on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await birth.wait() mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) @@ -1162,6 +1435,8 @@ async def wait_birth(topic, payload, qos): mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "homeassistant/status", mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, }, } ], @@ -1176,7 +1451,7 @@ async def wait_birth(topic, payload, qos): with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_mock._mqtt_on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await birth.wait() mqtt_client_mock.publish.assert_called_with( @@ -1191,7 +1466,7 @@ async def wait_birth(topic, payload, qos): async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): - mqtt_mock._mqtt_on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await asyncio.sleep(0.2) mqtt_client_mock.publish.assert_not_called() @@ -1205,26 +1480,27 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "homeassistant/status", mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, }, } ], ) -async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config): +async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_mock): """Test sending birth message does not happen until Home Assistant starts.""" hass.state = CoreState.starting birth = asyncio.Event() - result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: mqtt_config}) - assert result await hass.async_block_till_done() - # Workaround: asynctest==0.13 fails on @functools.lru_cache - spec = dir(hass.data["mqtt"]) - spec.remove("_matching_subscriptions") + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_component_mock = MagicMock( return_value=hass.data["mqtt"], - spec_set=spec, + spec_set=hass.data["mqtt"], wraps=hass.data["mqtt"], ) mqtt_component_mock._mqttc = mqtt_client_mock @@ -1239,7 +1515,7 @@ async def wait_birth(topic, payload, qos): with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) - mqtt_mock._mqtt_on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(birth.wait(), 0.2) @@ -1261,6 +1537,8 @@ async def wait_birth(topic, payload, qos): mqtt.CONF_WILL_MESSAGE: { mqtt.ATTR_TOPIC: "death", mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, }, } ], @@ -1306,7 +1584,7 @@ async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mo await mqtt.async_subscribe(hass, "still/pending", None, 1) hass.add_job = MagicMock() - mqtt_mock._mqtt_on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1317,9 +1595,28 @@ async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mo assert calls == expected -async def test_setup_fails_without_config(hass): - """Test if the MQTT component fails to load with no config.""" - assert not await async_setup_component(hass, mqtt.DOMAIN, {}) +async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mock): + """Test if the MQTT component loads with no config and config entry can be setup.""" + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + # mqtt present in yaml config + assert await async_setup_component(hass, mqtt.DOMAIN, {}) + + # User sets up a config entry + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + # Discover a device to verify the entry was setup correctly + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + assert device_entry is not None @pytest.mark.no_fail_on_log_exception @@ -1365,6 +1662,18 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): assert response["success"] +async def test_mqtt_ws_subscription_not_admin( + hass, hass_ws_client, mqtt_mock, hass_read_only_access_token +): + """Test MQTT websocket user is not admin.""" + client = await hass_ws_client(hass, access_token=hass_read_only_access_token) + await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) + response = await client.receive_json() + assert response["success"] is False + assert response["error"]["code"] == "unauthorized" + assert response["error"]["message"] == "Unauthorized" + + async def test_dump_service(hass, mqtt_mock): """Test that we can dump a topic.""" mopen = mock_open() @@ -1503,15 +1812,27 @@ async def test_mqtt_ws_get_device_debug_info( hass, device_reg, hass_ws_client, mqtt_mock ): """Test MQTT websocket device debug info.""" - config = { + config_sensor = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", "state_topic": "foobar/sensor", "unique_id": "unique", } - data = json.dumps(config) + config_trigger = { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "topic": "test-topic1", + "type": "foo", + "subtype": "bar", + } + data_sensor = json.dumps(config_sensor) + data_trigger = json.dumps(config_trigger) - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor) + async_fire_mqtt_message( + hass, "homeassistant/device_automation/bla/config", data_trigger + ) await hass.async_block_till_done() # Verify device entry is created @@ -1530,12 +1851,21 @@ async def test_mqtt_ws_get_device_debug_info( "entity_id": "sensor.mqtt_sensor", "subscriptions": [{"topic": "foobar/sensor", "messages": []}], "discovery_data": { - "payload": config, + "payload": config_sensor, "topic": "homeassistant/sensor/bla/config", }, + "transmitted": [], + } + ], + "triggers": [ + { + "discovery_data": { + "payload": config_trigger, + "topic": "homeassistant/device_automation/bla/config", + }, + "trigger_key": ["device_automation", "bla"], } ], - "triggers": [], } assert response["result"] == expected_result @@ -1595,6 +1925,7 @@ async def test_mqtt_ws_get_device_debug_info_binary( "payload": config, "topic": "homeassistant/camera/bla/config", }, + "transmitted": [], } ], "triggers": [], @@ -1662,7 +1993,7 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): device = registry.async_get_device({("mqtt", id)}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) if d["domain"] != "device_automation": assert len(debug_info_data["entities"]) == 1 assert len(debug_info_data["triggers"]) == 0 @@ -1739,7 +2070,7 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): device_id = config[0]["config"]["device"]["identifiers"][0] device = registry.async_get_device({("mqtt", device_id)}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 2 assert len(debug_info_data["triggers"]) == 2 @@ -1786,7 +2117,7 @@ async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "test"}}) - debug_info_data = await debug_info.info_for_device(hass, device_entry.id) + debug_info_data = debug_info.info_for_device(hass, device_entry.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 0 @@ -1810,7 +2141,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1821,7 +2152,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock): dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/abc", "123") - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert { "topic": "sensor/#", @@ -1856,7 +2187,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1871,7 +2202,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock): dt_utcnow.return_value = dt2 async_fire_mqtt_message(hass, "sensor/abc", "123") - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert len(debug_info_data["entities"][0]["subscriptions"][0]["messages"]) == 2 assert { @@ -1915,7 +2246,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert {"topic": "sensor/status", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1926,7 +2257,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock): dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert { "payload": "123", @@ -1966,7 +2297,7 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1979,7 +2310,7 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True) async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False) - debug_info_data = await debug_info.info_for_device(hass, device.id) + debug_info_data = debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert { "payload": "123", @@ -2069,3 +2400,38 @@ async def test_service_info_compatibility(hass, caplog): with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" assert "Detected integration that accessed discovery_info['topic']" in caplog.text + + +async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): + """Test connextion status subscription.""" + mqtt_connected_calls = [] + + @callback + async def async_mqtt_connected(status): + """Update state on connection/disconnection to MQTT broker.""" + mqtt_connected_calls.append(status) + + mqtt_mock.connected = True + + unsub = mqtt.async_subscribe_connection_status(hass, async_mqtt_connected) + await hass.async_block_till_done() + + # Mock connection status + mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + assert mqtt.is_connected(hass) is True + + # Mock disconnect status + mqtt_client_mock.on_disconnect(None, None, 0) + await hass.async_block_till_done() + + # Unsubscribe + unsub() + + mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + + # Check calls + assert len(mqtt_connected_calls) == 2 + assert mqtt_connected_calls[0] is True + assert mqtt_connected_calls[1] is False diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 59263037e657f..1667053f65b7a 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -51,6 +51,7 @@ 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, @@ -661,8 +662,8 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): """Test update of discovered vacuum.""" - config1 = {"name": "Beer", " " "command_topic": "test_topic"} - config2 = {"name": "Milk", " " "command_topic": "test_topic"} + config1 = {"name": "Beer", "command_topic": "test_topic"} + config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( hass, mqtt_mock, caplog, vacuum.DOMAIN, config1, config2 ) @@ -747,14 +748,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock): vacuum.DOMAIN: { "platform": "mqtt", "name": "test", - "battery_level_topic": "test-topic", + "battery_level_topic": "state-topic", "battery_level_template": "{{ value_json.battery_level }}", "command_topic": "command-topic", - "availability_topic": "avty-topic", + "payload_turn_on": "ON", } } await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, config, "test-topic" + hass, mqtt_mock, vacuum.DOMAIN, config, vacuum.SERVICE_TURN_ON ) @@ -840,6 +841,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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 + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index dcff826311bbc..ee59928c0c879 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -152,6 +152,37 @@ payload_on: "on" payload_off: "off" +Configuration with brightness command template: + +light: + platform: mqtt + name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_command_template: '{ "brightness": "{{ value }}" }' + qos: 0 + payload_on: "on" + payload_off: "off" + +Configuration with effect command template: + +light: + platform: mqtt + name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + effect_state_topic: "office/rgb1/effect/status" + effect_command_topic: "office/rgb1/effect/set" + effect_command_template: '{ "effect": "{{ value }}" }' + effect_list: + - rainbow + - colorloop + qos: 0 + payload_on: "on" + payload_off: "off" + """ import copy from unittest.mock import call, patch @@ -177,6 +208,7 @@ ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -201,6 +233,7 @@ 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, @@ -269,7 +302,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -298,6 +331,16 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] + async_fire_mqtt_message(hass, "test_light_rgb/status", "OFF") + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "None") + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): """Test the controlling of the state via topic for legacy light (white_value).""" @@ -332,7 +375,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -463,7 +506,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -581,7 +624,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -700,7 +743,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None @@ -823,7 +866,7 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -869,7 +912,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -909,7 +952,7 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -969,7 +1012,7 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert state.attributes.get("rgb_color") is None @@ -1016,6 +1059,10 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock state = hass.states.get("light.test") assert state.attributes.get("xy_color") == (0.14, 0.131) + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": null}') + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): """Test the setting of the state with a template.""" @@ -1058,7 +1105,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert state.attributes.get("rgb_color") is None @@ -1456,7 +1503,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 64]) @@ -1493,7 +1540,7 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 64, 32]) @@ -1530,7 +1577,7 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 64, 32, 16]) @@ -1566,7 +1613,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", color_temp=100) @@ -1599,7 +1646,7 @@ async def test_on_command_first(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=50) @@ -1634,7 +1681,7 @@ async def test_on_command_last(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=50) @@ -1671,7 +1718,7 @@ async def test_on_command_brightness(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN # Turn on w/ no brightness - should set to max await common.async_turn_on(hass, "light.test") @@ -1727,7 +1774,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN # Turn on w/ no brightness - should set to max await common.async_turn_on(hass, "light.test") @@ -1795,7 +1842,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -1885,7 +1932,7 @@ async def test_on_command_rgb(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -1975,7 +2022,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -2065,7 +2112,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -2156,7 +2203,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -2193,8 +2240,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF - + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) # Should get the following MQTT messages. @@ -2230,7 +2276,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", brightness=127) @@ -2279,7 +2325,7 @@ async def test_on_command_white(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert state.attributes.get("rgb_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None @@ -2364,7 +2410,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -2505,7 +2551,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None @@ -2591,7 +2637,7 @@ async def test_white_state_update(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert state.attributes.get("rgb_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None @@ -2639,7 +2685,7 @@ async def test_effect(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test", effect="rainbow") @@ -3329,7 +3375,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON ) @@ -3379,18 +3425,18 @@ async def test_max_mireds(hass, mqtt_mock): "brightness_command_topic", {"color_temp": "200", "brightness": "50"}, 50, - None, - None, - None, + "brightness_command_template", + "value", + b"5", ), ( light.SERVICE_TURN_ON, "effect_command_topic", {"rgb_color": [255, 128, 0], "effect": "color_loop"}, "color_loop", - None, - None, - None, + "effect_command_template", + "value", + b"c", ), ( light.SERVICE_TURN_ON, @@ -3482,6 +3528,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ @@ -3543,3 +3596,81 @@ async def test_encoding_subscribable_topics( attribute_value, init_payload, ) + + +async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): + """Test the sending of Brightness command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_brightness/set", + "brightness_command_topic": "test_light_brightness/brightness/set", + "brightness_command_template": "{{ (1000 / value) | round(0) }}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", brightness=100) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_brightness/set", "on", 0, False), + call("test_light_brightness/brightness/set", "10", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["brightness"] == 100 + + +async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): + """Test the sending of Effect command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_brightness/set", + "brightness_command_topic": "test_light_brightness/brightness/set", + "effect_command_topic": "test_light_brightness/effect/set", + "effect_command_template": '{ "effect": "{{ value }}" }', + "effect_list": ["colorloop", "random"], + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", effect="colorloop") + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_brightness/set", "on", 0, False), + call( + "test_light_brightness/effect/set", + '{ "effect": "colorloop" }', + 0, + False, + ), + ], + any_order=True, + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index baad644bf6db2..93bb9b0f57357 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -102,6 +102,7 @@ ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -126,6 +127,7 @@ 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, @@ -268,7 +270,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None @@ -291,6 +293,16 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb", '{"state": null}') + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + async def test_controlling_state_via_topic(hass, mqtt_mock): """Test the controlling of the state via topic.""" @@ -318,7 +330,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -446,7 +458,7 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -960,7 +972,7 @@ async def test_sending_hs_color(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN mqtt_mock.reset_mock() await common.async_turn_on( @@ -1023,7 +1035,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] @@ -1078,7 +1090,7 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] @@ -1155,7 +1167,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] @@ -1226,7 +1238,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] @@ -1296,7 +1308,7 @@ async def test_sending_xy_color(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] @@ -1359,7 +1371,7 @@ async def test_effect(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = ( light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION ) @@ -1422,7 +1434,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features @@ -1481,7 +1493,7 @@ async def test_transition(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features await common.async_turn_on(hass, "light.test", transition=15) @@ -1529,7 +1541,7 @@ async def test_brightness_scale(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("brightness") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -1573,7 +1585,7 @@ async def test_invalid_values(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN expected_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -1883,7 +1895,13 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, payload='{"state":"ON"}' + hass, + mqtt_mock, + light.DOMAIN, + DEFAULT_CONFIG, + light.SERVICE_TURN_ON, + command_payload='{"state": "ON"}', + state_payload='{"state":"ON"}', ) @@ -1974,6 +1992,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 3a8ecd357c328..4461cf14ef49a 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -40,6 +40,7 @@ ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -64,6 +65,7 @@ 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, @@ -171,6 +173,7 @@ async def test_rgb_light(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN expected_features = ( light.SUPPORT_TRANSITION | light.SUPPORT_COLOR @@ -208,7 +211,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -224,6 +227,16 @@ async def test_state_change_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("white_value") is None + async_fire_mqtt_message(hass, "test_light_rgb", "off") + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb", "None") + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + async def test_state_brightness_color_effect_temp_white_change_via_topic( hass, mqtt_mock @@ -264,7 +277,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("effect") is None @@ -283,6 +296,12 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert state.attributes.get("white_value") == 123 assert state.attributes.get("effect") is None + # make the light state unknown + async_fire_mqtt_message(hass, "test_light_rgb", "None") + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + # turn the light off async_fire_mqtt_message(hass, "test_light_rgb", "off") @@ -514,7 +533,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get("brightness") assert not state.attributes.get("hs_color") assert not state.attributes.get("effect") @@ -528,7 +547,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( @@ -536,7 +555,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN # Set color_temp await common.async_turn_on(hass, "light.test", color_temp=70) @@ -545,7 +564,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get("color_temp") # Set full brightness @@ -555,7 +574,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get("brightness") # Full brightness - no scaling of RGB values sent over MQTT @@ -567,7 +586,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get("white_value") assert not state.attributes.get("rgb_color") @@ -628,7 +647,7 @@ async def test_effect(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 await common.async_turn_on(hass, "light.test") @@ -679,7 +698,7 @@ async def test_flash(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 await common.async_turn_on(hass, "light.test") @@ -727,7 +746,7 @@ async def test_transition(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 @@ -783,7 +802,7 @@ async def test_invalid_values(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None @@ -1064,12 +1083,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock): "schema": "template", "name": "test", "command_topic": "test-topic", - "command_on_template": "on,{{ transition }}", + "command_on_template": "ON", "command_off_template": "off,{{ transition|d }}", "state_template": '{{ value.split(",")[0] }}', } } - await help_test_entity_debug_info_message(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_debug_info_message( + hass, mqtt_mock, light.DOMAIN, config, light.SERVICE_TURN_ON + ) async def test_max_mireds(hass, mqtt_mock): @@ -1161,6 +1182,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value,init_payload", [ diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index f29222f97d58e..86e21a261a332 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -40,6 +40,7 @@ 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, @@ -590,7 +591,12 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock, + LOCK_DOMAIN, + DEFAULT_CONFIG, + SERVICE_LOCK, + command_payload="LOCK", ) @@ -641,6 +647,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index c233bf14ab517..73c1da357fa48 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -46,6 +46,7 @@ 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, @@ -541,7 +542,14 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload="1" + hass, + mqtt_mock, + number.DOMAIN, + DEFAULT_CONFIG, + SERVICE_SET_VALUE, + service_parameters={ATTR_VALUE: 45}, + command_payload="45", + state_payload="1", ) @@ -692,6 +700,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 97f13ba90c09a..1ccacc1c5eec9 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -19,6 +19,7 @@ help_test_discovery_update, help_test_discovery_update_unchanged, help_test_reloadable, + help_test_reloadable_late, help_test_unique_id, ) @@ -183,3 +184,10 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): domain = scene.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c09c0aebca83b..069c0dfe4c9fc 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -37,6 +37,7 @@ 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, @@ -475,7 +476,14 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, payload="milk" + hass, + mqtt_mock, + select.DOMAIN, + DEFAULT_CONFIG, + select.SERVICE_SELECT_OPTION, + service_parameters={ATTR_OPTION: "beer"}, + command_payload="beer", + state_payload="milk", ) @@ -571,6 +579,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index c758b670b3d37..b653e04c82ee4 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -2,13 +2,19 @@ import copy from datetime import datetime, timedelta import json -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + EVENT_STATE_CHANGED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) import homeassistant.core as ha from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -45,6 +51,7 @@ 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, @@ -906,7 +913,7 @@ async def test_entity_debug_info_max_messages(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None ) @@ -972,6 +979,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + async def test_cleanup_triggers_and_restoring_state( hass, mqtt_mock, caplog, tmp_path, freezer ): @@ -981,10 +995,15 @@ async def test_cleanup_triggers_and_restoring_state( config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" + config1["device_class"] = "temperature" + config1["unit_of_measurement"] = TEMP_FAHRENHEIT + config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" + config2["device_class"] = "temperature" + config2["unit_of_measurement"] = TEMP_CELSIUS freezer.move_to("2022-02-02 12:01:00+01:00") @@ -996,7 +1015,7 @@ async def test_cleanup_triggers_and_restoring_state( await hass.async_block_till_done() async_fire_mqtt_message(hass, "test-topic1", "100") state = hass.states.get("sensor.test1") - assert state.state == "100" + assert state.state == "38" # 100 °F -> 38 °C async_fire_mqtt_message(hass, "test-topic2", "200") state = hass.states.get("sensor.test2") @@ -1018,14 +1037,14 @@ async def test_cleanup_triggers_and_restoring_state( assert "State recovered after reload for sensor.test2" not in caplog.text state = hass.states.get("sensor.test1") - assert state.state == "100" + assert state.state == "38" # 100 °F -> 38 °C state = hass.states.get("sensor.test2") assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "test-topic1", "101") + async_fire_mqtt_message(hass, "test-topic1", "80") state = hass.states.get("sensor.test1") - assert state.state == "101" + assert state.state == "27" # 80 °F -> 27 °C async_fire_mqtt_message(hass, "test-topic2", "201") state = hass.states.get("sensor.test2") @@ -1049,10 +1068,16 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( {}, last_changed=datetime.fromisoformat("2022-02-02 12:01:35+01:00"), ) + fake_extra_data = MagicMock() with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=fake_state, - ), assert_setup_component(1, domain): + ), patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data", + return_value=fake_extra_data, + ), assert_setup_component( + 1, domain + ): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1079,4 +1104,5 @@ async def test_encoding_subscribable_topics( value, attribute, attribute_value, + skip_raw_test=True, ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py new file mode 100644 index 0000000000000..f39154badc95b --- /dev/null +++ b/tests/components/mqtt/test_siren.py @@ -0,0 +1,897 @@ +"""The tests for the MQTT siren platform.""" +import copy +from unittest.mock import patch + +import pytest + +from homeassistant.components import siren +from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +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_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, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + siren.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} +} + + +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, parameters={}) -> None: + """Turn all or specified siren on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data.update(parameters) + + await hass.services.async_call(siren.DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: + """Turn all or specified siren off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + await hass.services.async_call(siren.DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + siren.DOMAIN, + { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("siren.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "1") + + state = hass.states.get("siren.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", "0") + + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + + +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test the sending MQTT commands in optimistic mode.""" + assert await async_setup_component( + hass, + siren.DOMAIN, + { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on(hass, entity_id="siren.test") + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{"state": "beer on"}', 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("siren.test") + assert state.state == STATE_ON + + await async_turn_off(hass, entity_id="siren.test") + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{"state": "beer off"}', 2, False + ) + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component( + hass, + siren.DOMAIN, + { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "state_value_template": "{{ value_json.val }}", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("siren.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "state-topic", '{"val":"beer on"}') + + state = hass.states.get("siren.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", '{"val": null }') + state = hass.states.get("siren.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "state-topic", '{"val":"beer off"}') + + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + + +async def test_controlling_state_and_attributes_with_json_message_without_template( + hass, mqtt_mock, caplog +): + """Test the controlling state via topic and JSON message without a value template.""" + assert await async_setup_component( + hass, + siren.DOMAIN, + { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "available_tones": ["ping", "siren", "bell"], + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("siren.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(siren.ATTR_TONE) is None + assert state.attributes.get(siren.ATTR_DURATION) is None + assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) is None + + async_fire_mqtt_message( + hass, + "state-topic", + '{"state":"beer on", "tone": "bell", "duration": 10, "volume_level": 0.5 }', + ) + + state = hass.states.get("siren.test") + assert state.state == STATE_ON + assert state.attributes.get(siren.ATTR_TONE) == "bell" + assert state.attributes.get(siren.ATTR_DURATION) == 10 + assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.5 + + async_fire_mqtt_message( + hass, + "state-topic", + '{"state":"beer off", "duration": 5, "volume_level": 0.6}', + ) + + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + assert state.attributes.get(siren.ATTR_TONE) == "bell" + assert state.attributes.get(siren.ATTR_DURATION) == 5 + assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.6 + + # Test validation of received attributes, invalid + async_fire_mqtt_message( + hass, + "state-topic", + '{"state":"beer on", "duration": 6, "volume_level": 2 }', + ) + state = hass.states.get("siren.test") + assert ( + "Unable to update siren state attributes from payload '{'duration': 6, 'volume_level': 2}': value must be at most 1 for dictionary value @ data['volume_level']" + in caplog.text + ) + assert state.state == STATE_OFF + assert state.attributes.get(siren.ATTR_TONE) == "bell" + assert state.attributes.get(siren.ATTR_DURATION) == 5 + assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.6 + + async_fire_mqtt_message( + hass, + "state-topic", + "{}", + ) + assert state.state == STATE_OFF + assert state.attributes.get(siren.ATTR_TONE) == "bell" + assert state.attributes.get(siren.ATTR_DURATION) == 5 + assert state.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.6 + assert ( + "Ignoring empty payload '{}' after rendering for topic state-topic" + in caplog.text + ) + + +async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): + """Test setting attributes with support flags optimistic.""" + config = { + "platform": "mqtt", + "command_topic": "command-topic", + "available_tones": ["ping", "siren", "bell"], + } + config1 = copy.deepcopy(config) + config1["name"] = "test1" + config1["support_duration"] = False + config2 = copy.deepcopy(config) + config2["name"] = "test2" + config2["support_volume_set"] = False + config3 = copy.deepcopy(config) + config3["name"] = "test3" + del config3["available_tones"] + + assert await async_setup_component( + hass, + siren.DOMAIN, + {siren.DOMAIN: [config1, config2, config3]}, + ) + await hass.async_block_till_done() + + state1 = hass.states.get("siren.test1") + assert state1.state == STATE_OFF + assert siren.ATTR_DURATION not in state1.attributes + assert siren.ATTR_AVAILABLE_TONES in state1.attributes + assert siren.ATTR_TONE in state1.attributes + assert siren.ATTR_VOLUME_LEVEL in state1.attributes + await async_turn_on( + hass, + entity_id="siren.test1", + parameters={ + siren.ATTR_DURATION: 22, + siren.ATTR_TONE: "ping", + ATTR_VOLUME_LEVEL: 0.88, + }, + ) + state1 = hass.states.get("siren.test1") + assert state1.attributes.get(siren.ATTR_TONE) == "ping" + assert state1.attributes.get(siren.ATTR_DURATION) is None + assert state1.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + + state2 = hass.states.get("siren.test2") + assert siren.ATTR_DURATION in state2.attributes + assert siren.ATTR_AVAILABLE_TONES in state2.attributes + assert siren.ATTR_TONE in state2.attributes + assert siren.ATTR_VOLUME_LEVEL not in state2.attributes + await async_turn_on( + hass, + entity_id="siren.test2", + parameters={ + siren.ATTR_DURATION: 22, + siren.ATTR_TONE: "ping", + ATTR_VOLUME_LEVEL: 0.88, + }, + ) + state2 = hass.states.get("siren.test2") + assert state2.attributes.get(siren.ATTR_TONE) == "ping" + assert state2.attributes.get(siren.ATTR_DURATION) == 22 + assert state2.attributes.get(siren.ATTR_VOLUME_LEVEL) is None + + state3 = hass.states.get("siren.test3") + assert siren.ATTR_DURATION in state3.attributes + assert siren.ATTR_AVAILABLE_TONES not in state3.attributes + assert siren.ATTR_TONE not in state3.attributes + assert siren.ATTR_VOLUME_LEVEL in state3.attributes + await async_turn_on( + hass, + entity_id="siren.test3", + parameters={ + siren.ATTR_DURATION: 22, + siren.ATTR_TONE: "ping", + ATTR_VOLUME_LEVEL: 0.88, + }, + ) + state3 = hass.states.get("siren.test3") + assert state3.attributes.get(siren.ATTR_TONE) is None + assert state3.attributes.get(siren.ATTR_DURATION) == 22 + assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + + +async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): + """Test setting attributes with support flags via state.""" + config = { + "platform": "mqtt", + "command_topic": "command-topic", + "available_tones": ["ping", "siren", "bell"], + } + config1 = copy.deepcopy(config) + config1["name"] = "test1" + config1["state_topic"] = "state-topic1" + config1["support_duration"] = False + config2 = copy.deepcopy(config) + config2["name"] = "test2" + config2["state_topic"] = "state-topic2" + config2["support_volume_set"] = False + config3 = copy.deepcopy(config) + config3["name"] = "test3" + config3["state_topic"] = "state-topic3" + del config3["available_tones"] + + assert await async_setup_component( + hass, + siren.DOMAIN, + {siren.DOMAIN: [config1, config2, config3]}, + ) + await hass.async_block_till_done() + + state1 = hass.states.get("siren.test1") + assert state1.state == STATE_UNKNOWN + assert siren.ATTR_DURATION not in state1.attributes + assert siren.ATTR_AVAILABLE_TONES in state1.attributes + assert siren.ATTR_TONE in state1.attributes + assert siren.ATTR_VOLUME_LEVEL in state1.attributes + async_fire_mqtt_message( + hass, + "state-topic1", + '{"state":"ON", "duration": 22, "tone": "ping", "volume_level": 0.88}', + ) + await hass.async_block_till_done() + state1 = hass.states.get("siren.test1") + assert state1.attributes.get(siren.ATTR_TONE) == "ping" + assert state1.attributes.get(siren.ATTR_DURATION) is None + assert state1.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + + state2 = hass.states.get("siren.test2") + assert siren.ATTR_DURATION in state2.attributes + assert siren.ATTR_AVAILABLE_TONES in state2.attributes + assert siren.ATTR_TONE in state2.attributes + assert siren.ATTR_VOLUME_LEVEL not in state2.attributes + async_fire_mqtt_message( + hass, + "state-topic2", + '{"state":"ON", "duration": 22, "tone": "ping", "volume_level": 0.88}', + ) + await hass.async_block_till_done() + state2 = hass.states.get("siren.test2") + assert state2.attributes.get(siren.ATTR_TONE) == "ping" + assert state2.attributes.get(siren.ATTR_DURATION) == 22 + assert state2.attributes.get(siren.ATTR_VOLUME_LEVEL) is None + + state3 = hass.states.get("siren.test3") + assert siren.ATTR_DURATION in state3.attributes + assert siren.ATTR_AVAILABLE_TONES not in state3.attributes + assert siren.ATTR_TONE not in state3.attributes + assert siren.ATTR_VOLUME_LEVEL in state3.attributes + async_fire_mqtt_message( + hass, + "state-topic3", + '{"state":"ON", "duration": 22, "tone": "ping", "volume_level": 0.88}', + ) + await hass.async_block_till_done() + state3 = hass.states.get("siren.test3") + assert state3.attributes.get(siren.ATTR_TONE) is None + assert state3.attributes.get(siren.ATTR_DURATION) == 22 + assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + config = { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } + } + + await help_test_default_availability_payload( + hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } + } + + await help_test_custom_availability_payload( + hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + ) + + +async def test_custom_state_payload(hass, mqtt_mock): + """Test the state payload.""" + assert await async_setup_component( + hass, + siren.DOMAIN, + { + siren.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "state_on": "HIGH", + "state_off": "LOW", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("siren.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "HIGH") + + state = hass.states.get("siren.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "state-topic", "LOW") + + state = hass.states.get("siren.test") + assert state.state == STATE_OFF + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG, {} + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one siren per unique_id.""" + config = { + siren.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, siren.DOMAIN, config) + + +async def test_discovery_removal_siren(hass, mqtt_mock, caplog): + """Test removal of discovered siren.""" + data = ( + '{ "name": "test",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + await help_test_discovery_removal(hass, mqtt_mock, caplog, siren.DOMAIN, data) + + +async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): + """Test update of discovered siren.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "siren/state1" + config2["state_topic"] = "siren/state2" + config1["state_value_template"] = "{{ value_json.state1.state }}" + config2["state_value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("siren/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("siren/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ([("siren/state2", '{"state2":{"state":"ON"}}')], "on", None), + ([("siren/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("siren/state1", '{"state2":{"state":"OFF"}}')], "on", None), + ([("siren/state2", '{"state1":{"state":"OFF"}}')], "on", None), + ([("siren/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + siren.DOMAIN, + config1, + config2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): + """Test update of discovered siren.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "siren/state1" + config2["state_topic"] = "siren/state1" + config1["state_value_template"] = "{{ value_json.state1.state }}" + config2["state_value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("siren/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("siren/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ([("siren/state1", '{"state2":{"state":"ON"}}')], "on", None), + ([("siren/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("siren/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + siren.DOMAIN, + config1, + config2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_command_templates(hass, mqtt_mock, caplog): + """Test siren with command templates optimistic.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1["name"] = "Beer" + config1["available_tones"] = ["ping", "chimes"] + config1[ + "command_template" + ] = "CMD: {{ value }}, DURATION: {{ duration }}, TONE: {{ tone }}, VOLUME: {{ volume_level }}" + + config2 = copy.deepcopy(config1) + config2["name"] = "Milk" + config2["command_off_template"] = "CMD_OFF: {{ value }}" + + assert await async_setup_component( + hass, + siren.DOMAIN, + {siren.DOMAIN: [config1, config2]}, + ) + await hass.async_block_till_done() + + state1 = hass.states.get("siren.beer") + assert state1.state == STATE_OFF + assert state1.attributes.get(ATTR_ASSUMED_STATE) + + state2 = hass.states.get("siren.milk") + assert state2.state == STATE_OFF + assert state1.attributes.get(ATTR_ASSUMED_STATE) + + await async_turn_on( + hass, + entity_id="siren.beer", + parameters={ + siren.ATTR_DURATION: 22, + siren.ATTR_TONE: "ping", + ATTR_VOLUME_LEVEL: 0.88, + }, + ) + state1 = hass.states.get("siren.beer") + assert state1.attributes.get(siren.ATTR_TONE) == "ping" + assert state1.attributes.get(siren.ATTR_DURATION) == 22 + assert state1.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + + mqtt_mock.async_publish.assert_any_call( + "test-topic", "CMD: ON, DURATION: 22, TONE: ping, VOLUME: 0.88", 0, False + ) + mqtt_mock.async_publish.call_count == 1 + mqtt_mock.reset_mock() + await async_turn_off( + hass, + entity_id="siren.beer", + ) + mqtt_mock.async_publish.assert_any_call( + "test-topic", "CMD: OFF, DURATION: , TONE: , VOLUME:", 0, False + ) + mqtt_mock.async_publish.call_count == 1 + mqtt_mock.reset_mock() + + await async_turn_on( + hass, + entity_id="siren.milk", + parameters={ + siren.ATTR_DURATION: 22, + siren.ATTR_TONE: "ping", + ATTR_VOLUME_LEVEL: 0.88, + }, + ) + state2 = hass.states.get("siren.milk") + assert state2.attributes.get(siren.ATTR_TONE) == "ping" + assert state2.attributes.get(siren.ATTR_DURATION) == 22 + assert state2.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 + await async_turn_off( + hass, + entity_id="siren.milk", + ) + mqtt_mock.async_publish.assert_any_call("test-topic", "CMD_OFF: OFF", 0, False) + mqtt_mock.async_publish.call_count == 1 + mqtt_mock.reset_mock() + + +async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): + """Test update of discovered siren.""" + data1 = ( + '{ "name": "Beer",' + ' "device_class": "siren",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.siren.MqttSiren.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, siren.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = ( + '{ "name": "Milk",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + await help_test_discovery_broken( + hass, mqtt_mock, caplog, siren.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT siren device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT siren device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock, + siren.DOMAIN, + DEFAULT_CONFIG, + siren.SERVICE_TURN_ON, + command_payload='{"state": "ON"}', + ) + + +@pytest.mark.parametrize( + "service,topic,parameters,payload,template", + [ + ( + siren.SERVICE_TURN_ON, + "command_topic", + None, + '{"state": "ON"}', + None, + ), + ( + siren.SERVICE_TURN_OFF, + "command_topic", + None, + '{"state": "OFF"}', + None, + ), + ], +) +async def test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + service, + topic, + parameters, + payload, + template, +): + """Test publishing MQTT payload with command templates and different encoding.""" + domain = siren.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config[siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + +async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): + """Test reloading the MQTT platform.""" + domain = siren.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + + +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +@pytest.mark.parametrize( + "topic,value,attribute,attribute_value", + [ + ("state_topic", "ON", None, "on"), + ], +) +async def test_encoding_subscribable_topics( + hass, mqtt_mock, caplog, topic, value, attribute, attribute_value +): + """Test handling of incoming encoded payload.""" + await help_test_encoding_subscribable_topics( + hass, + mqtt_mock, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG[siren.DOMAIN], + topic, + value, + attribute, + attribute_value, + ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 5011f279470ec..8691aa73323e6 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -54,6 +54,7 @@ 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, @@ -235,6 +236,8 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + state = hass.states.get("vacuum.mqtttest") + assert state.state == STATE_UNKNOWN message = """{ "battery_level": 54, @@ -262,6 +265,11 @@ async def test_status(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "min" assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] + message = '{"state":null}' + async_fire_mqtt_message(hass, "vacuum/state", message) + state = hass.states.get("vacuum.mqtttest") + assert state.state == STATE_UNKNOWN + async def test_no_fan_vacuum(hass, mqtt_mock): """Test status updates from the vacuum when fan is not supported.""" @@ -503,7 +511,13 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, payload="{}" + hass, + mqtt_mock, + vacuum.DOMAIN, + DEFAULT_CONFIG_2, + vacuum.SERVICE_START, + command_payload="start", + state_payload="{}", ) @@ -594,6 +608,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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 + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 36d8946be0bb2..e2ffc602ddd44 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -2,6 +2,7 @@ from unittest.mock import ANY from homeassistant.components.mqtt.subscription import ( + async_prepare_subscribe_topics, async_subscribe_topics, async_unsubscribe_topics, ) @@ -27,7 +28,7 @@ def record_calls2(*args): calls2.append(args) sub_state = None - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, { @@ -35,6 +36,7 @@ def record_calls2(*args): "test_topic2": {"topic": "test-topic2", "msg_callback": record_calls2}, }, ) + await async_subscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1", "test-payload1") assert len(calls1) == 1 @@ -48,7 +50,7 @@ def record_calls2(*args): assert calls2[0][0].topic == "test-topic2" assert calls2[0][0].payload == "test-payload2" - await async_unsubscribe_topics(hass, sub_state) + async_unsubscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1", "test-payload") async_fire_mqtt_message(hass, "test-topic2", "test-payload") @@ -74,7 +76,7 @@ def record_calls2(*args): calls2.append(args) sub_state = None - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, { @@ -82,6 +84,7 @@ def record_calls2(*args): "test_topic2": {"topic": "test-topic2", "msg_callback": record_calls2}, }, ) + await async_subscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1", "test-payload") assert len(calls1) == 1 @@ -91,11 +94,12 @@ def record_calls2(*args): assert len(calls1) == 1 assert len(calls2) == 1 - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, {"test_topic1": {"topic": "test-topic1_1", "msg_callback": record_calls1}}, ) + await async_subscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1", "test-payload") async_fire_mqtt_message(hass, "test-topic2", "test-payload") @@ -108,7 +112,7 @@ def record_calls2(*args): assert calls1[1][0].payload == "test-payload" assert len(calls2) == 1 - await async_unsubscribe_topics(hass, sub_state) + async_unsubscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1_1", "test-payload") async_fire_mqtt_message(hass, "test-topic2", "test-payload") @@ -126,11 +130,12 @@ def msg_callback(*args): pass sub_state = None - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, {"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}}, ) + await async_subscribe_topics(hass, sub_state) mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 0, "utf-8") @@ -143,7 +148,7 @@ def msg_callback(*args): pass sub_state = None - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, { @@ -155,6 +160,7 @@ def msg_callback(*args): } }, ) + await async_subscribe_topics(hass, sub_state) mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 1, "utf-16") @@ -169,27 +175,29 @@ def record_calls(*args): calls.append(args) sub_state = None - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, {"test_topic1": {"topic": "test-topic1", "msg_callback": record_calls}}, ) + await async_subscribe_topics(hass, sub_state) subscribe_call_count = mqtt_mock.async_subscribe.call_count async_fire_mqtt_message(hass, "test-topic1", "test-payload") assert len(calls) == 1 - sub_state = await async_subscribe_topics( + sub_state = async_prepare_subscribe_topics( hass, sub_state, {"test_topic1": {"topic": "test-topic1", "msg_callback": record_calls}}, ) + await async_subscribe_topics(hass, sub_state) assert subscribe_call_count == mqtt_mock.async_subscribe.call_count async_fire_mqtt_message(hass, "test-topic1", "test-payload") assert len(calls) == 2 - await async_unsubscribe_topics(hass, sub_state) + async_unsubscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, "test-topic1", "test-payload") assert len(calls) == 2 diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 9519d7321ffdd..a458ac03baa14 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -11,6 +11,7 @@ ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, + STATE_UNKNOWN, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -35,6 +36,7 @@ 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, @@ -71,7 +73,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_DEVICE_CLASS) == "switch" assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -85,6 +87,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get("switch.test") assert state.state == STATE_OFF + async_fire_mqtt_message(hass, "state-topic", "None") + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending MQTT commands in optimistic mode.""" @@ -132,6 +139,26 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF +async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): + """Test the initial state in optimistic mode.""" + assert await async_setup_component( + hass, + switch.DOMAIN, + { + switch.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -152,7 +179,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, "state-topic", '{"val":"beer on"}') @@ -164,6 +191,11 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): state = hass.states.get("switch.test") assert state.state == STATE_OFF + async_fire_mqtt_message(hass, "state-topic", '{"val": null}') + + state = hass.states.get("switch.test") + assert state.state == STATE_UNKNOWN + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" @@ -236,7 +268,7 @@ async def test_custom_state_payload(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get("switch.test") - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "HIGH") @@ -468,7 +500,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, switch.SERVICE_TURN_ON ) @@ -526,6 +558,13 @@ async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) +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[domain] + await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + @pytest.mark.parametrize( "topic,value,attribute,attribute_value", [ diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index e1f3de83a0d05..7d3b4f2e1b205 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -7,8 +7,10 @@ from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_get_device_automations, mock_device_registry, @@ -355,11 +357,15 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, + hass_ws_client, device_reg, mqtt_mock, tag_mock, ): """Test tag scanning after removal.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -371,9 +377,16 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( await hass.async_block_till_done() tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) - # Remove the device - device_reg.async_remove_device(device_entry.id) - await hass.async_block_till_done() + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "device_id": device_entry.id, + } + ) + response = await ws_client.receive_json() + assert response["success"] tag_mock.reset_mock() async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -473,32 +486,78 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): """Test tag discovery topic is cleaned when device is removed from registry.""" - config = { + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + mqtt_entry = hass.config_entries.async_entries("mqtt")[0] + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + + device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections=set(), + identifiers={("mqtt", "helloworld")}, + ) + + config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, } + config2 = { + "topic": "test-topic", + "device": {"identifiers": ["hejhopp"]}, + } - data = json.dumps(config) - async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) + data1 = json.dumps(config1) + data2 = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/tag/bla2/config", data2) await hass.async_block_till_done() - # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) - assert device_entry is not None + # Verify device registry entries are created + device_entry1 = device_reg.async_get_device({("mqtt", "helloworld")}) + assert device_entry1 is not None + assert device_entry1.config_entries == {config_entry.entry_id, mqtt_entry.entry_id} + device_entry2 = device_reg.async_get_device({("mqtt", "hejhopp")}) + assert device_entry2 is not None - device_reg.async_remove_device(device_entry.id) + # Remove other config entry from the device + device_reg.async_update_device( + device_entry1.id, remove_config_entry_id=config_entry.entry_id + ) + device_entry1 = device_reg.async_get_device({("mqtt", "helloworld")}) + assert device_entry1 is not None + assert device_entry1.config_entries == {mqtt_entry.entry_id} + device_entry2 = device_reg.async_get_device({("mqtt", "hejhopp")}) + assert device_entry2 is not None + mqtt_mock.async_publish.assert_not_called() + + # Remove MQTT from the device + await ws_client.send_json( + { + "id": 6, + "type": "mqtt/device/remove", + "device_id": device_entry1.id, + } + ) + response = await ws_client.receive_json() + assert response["success"] await hass.async_block_till_done() await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) - assert device_entry is None + device_entry1 = device_reg.async_get_device({("mqtt", "helloworld")}) + assert device_entry1 is None + device_entry2 = device_reg.async_get_device({("mqtt", "hejhopp")}) + assert device_entry2 is not None # Verify retained discovery topic has been cleared mqtt_mock.async_publish.assert_called_once_with( - "homeassistant/tag/bla/config", "", 0, True + "homeassistant/tag/bla1/config", "", 0, True ) diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 6dd7add37e6c9..fe98b3f7e0a65 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -6,6 +6,7 @@ from typing import Any from unittest.mock import MagicMock, patch +from mysensors import BaseSyncGateway from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest @@ -142,6 +143,12 @@ def receive_message(message_string: str) -> None: yield config_entry, receive_message +@pytest.fixture(name="gateway") +def gateway_fixture(transport, integration) -> BaseSyncGateway: + """Return a setup gateway.""" + return transport.call_args[0][0] + + def load_nodes_state(fixture_path: str) -> dict: """Load mysensors nodes fixture.""" return json.loads(load_fixture(fixture_path), cls=MySensorsJSONDecoder) diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 7c83334d8f3b9..6f97c312ec0cd 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -1,9 +1,13 @@ """Test function in __init__.py.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable +from typing import Any, Awaitable from unittest.mock import patch +from aiohttp import ClientWebSocketResponse +from mysensors import BaseSyncGateway +from mysensors.sensor import Sensor import pytest from homeassistant.components.mysensors import ( @@ -27,9 +31,12 @@ CONF_TOPIC_OUT_PREFIX, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + @pytest.mark.parametrize( "config, expected_calls, expected_to_succeed, expected_config_entry_data", @@ -347,3 +354,51 @@ async def test_import( persistence_path = config_entry_data.pop(CONF_PERSISTENCE_FILE) assert persistence_path == expected_persistence_path assert config_entry_data == expected_config_entry_data[idx] + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + gps_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], + gateway: BaseSyncGateway, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that a device can be removed ok.""" + entity_id = "sensor.gps_sensor_1_1" + node_id = 1 + config_entry, _ = integration + assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, f"{config_entry.entry_id}-{node_id}")} + ) + entity_registry = er.async_get(hass) + state = hass.states.get(entity_id) + + assert gateway.sensors + assert gateway.sensors[node_id] + assert device_entry + assert state + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert node_id not in gateway.sensors + assert gateway.tasks.persistence.need_save is True + assert not device_registry.async_get_device( + identifiers={(DOMAIN, f"{config_entry.entry_id}-1")} + ) + assert not entity_registry.async_get(entity_id) + assert not hass.states.get(entity_id) diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 015c645a3e776..9479e29cdeafd 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -14,6 +14,7 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="10.10.2.3", + addresses=["10.10.2.3"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 4e4a48e9bfe5d..305f88a2e9089 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -238,6 +238,7 @@ async def test_discovery_link_unavailable( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery_info}", port=None, @@ -422,6 +423,7 @@ async def test_import_discovery_integration( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery}", port=None, diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index a4539cf9f8172..b64e251bcf0b4 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -7,10 +7,9 @@ import datetime from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import aiohttp -from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage import pytest @@ -21,19 +20,20 @@ STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, ) +from homeassistant.components.nest.const import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import async_setup_sdm_platform +from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .conftest import FakeAuth from tests.common import async_fire_time_changed PLATFORM = "camera" CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" -DEVICE_ID = "some-device-id" DEVICE_TRAITS = { "sdm.devices.traits.Info": { "customName": "My Camera", @@ -50,13 +50,9 @@ "sdm.devices.traits.CameraMotion": {}, } DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS" -DOMAIN = "nest" MOTION_EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." -# Tests can assert that image bytes came from an event or was decoded -# from the live stream. -IMAGE_BYTES_FROM_EVENT = b"test url image bytes" IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." @@ -69,6 +65,45 @@ IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} +@pytest.fixture +def platforms() -> list[str]: + """Fixture to set platforms used in the test.""" + return ["camera"] + + +@pytest.fixture +async def device_type() -> str: + """Fixture to set default device type used when creating devices.""" + return "sdm.devices.types.CAMERA" + + +@pytest.fixture +def camera_device(create_device: CreateDevice) -> None: + """Fixture to create a basic camera device.""" + create_device.create(DEVICE_TRAITS) + + +@pytest.fixture +def webrtc_camera_device(create_device: CreateDevice) -> None: + """Fixture to create a WebRTC camera device.""" + create_device.create( + { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC"], + }, + } + ) + + def make_motion_event( event_id: str = MOTION_EVENT_ID, event_session_id: str = EVENT_SESSION_ID, @@ -116,19 +151,28 @@ def make_stream_url_response( ) -async def async_setup_camera(hass, traits={}, auth=None): - """Set up the platform and prerequisites.""" - devices = {} - if traits: - devices[DEVICE_ID] = Device.MakeDevice( - { - "name": DEVICE_ID, - "type": CAMERA_DEVICE_TYPE, - "traits": traits, - }, - auth=auth, +@pytest.fixture +async def mock_create_stream(hass) -> Mock: + """Fixture to mock out the create stream call.""" + assert await async_setup_component(hass, "stream", {}) + with patch( + "homeassistant.components.camera.create_stream", autospec=True + ) as mock_stream: + mock_stream.return_value.endpoint_url.return_value = ( + "http://home.assistant/playlist.m3u8" ) - return await async_setup_sdm_platform(hass, PLATFORM, devices) + mock_stream.return_value.async_get_image = AsyncMock() + mock_stream.return_value.async_get_image.return_value = IMAGE_BYTES_FROM_STREAM + yield mock_stream + + +async def async_get_image(hass, width=None, height=None): + """Get the camera image.""" + image = await camera.async_get_image( + hass, "camera.my_camera", width=width, height=height + ) + assert image.content_type == "image/jpeg" + return image.content async def fire_alarm(hass, point_in_time): @@ -138,43 +182,33 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -async def async_get_image(hass, width=None, height=None): - """Get image from the camera, a wrapper around camera.async_get_image.""" - # Note: this patches ImageFrame to simulate decoding an image from a live - # stream, however the test may not use it. Tests assert on the image - # contents to determine if the image came from the live stream or event. - with patch( - "homeassistant.components.ffmpeg.ImageFrame.get_image", - autopatch=True, - return_value=IMAGE_BYTES_FROM_STREAM, - ): - return await camera.async_get_image( - hass, "camera.my_camera", width=width, height=height - ) - - -async def test_no_devices(hass): +async def test_no_devices(hass: HomeAssistant, setup_platform: PlatformSetup): """Test configuration that returns no devices.""" - await async_setup_camera(hass) + await setup_platform() assert len(hass.states.async_all()) == 0 -async def test_ineligible_device(hass): +async def test_ineligible_device( + hass: HomeAssistant, setup_platform: PlatformSetup, create_device: CreateDevice +): """Test configuration with devices that do not support cameras.""" - await async_setup_camera( - hass, + create_device.create( { "sdm.devices.traits.Info": { "customName": "My Camera", }, - }, + } ) + + await setup_platform() assert len(hass.states.async_all()) == 0 -async def test_camera_device(hass): +async def test_camera_device( + hass: HomeAssistant, setup_platform: PlatformSetup, camera_device: None +): """Test a basic camera with a live stream.""" - await async_setup_camera(hass, DEVICE_TRAITS) + await setup_platform() assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.my_camera") @@ -183,7 +217,7 @@ async def test_camera_device(hass): registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") - assert entry.unique_id == "some-device-id-camera" + assert entry.unique_id == f"{DEVICE_ID}-camera" assert entry.original_name == "My Camera" assert entry.domain == "camera" @@ -194,10 +228,16 @@ async def test_camera_device(hass): assert device.identifiers == {("nest", DEVICE_ID)} -async def test_camera_stream(hass, auth): +async def test_camera_stream( + hass: HomeAssistant, + setup_platform: PlatformSetup, + camera_device: None, + auth: FakeAuth, + mock_create_stream: Mock, +): """Test a basic camera and fetch its live stream.""" auth.responses = [make_stream_url_response()] - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -208,14 +248,20 @@ async def test_camera_stream(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM + assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM -async def test_camera_ws_stream(hass, auth, hass_ws_client): +async def test_camera_ws_stream( + hass, + setup_platform, + camera_device, + hass_ws_client, + auth, + mock_create_stream, +): """Test a basic camera that supports web rtc.""" auth.responses = [make_stream_url_response()] - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -223,28 +269,30 @@ async def test_camera_ws_stream(hass, auth, hass_ws_client): assert cam.state == STATE_STREAMING assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_HLS - with patch("homeassistant.components.camera.create_stream") as mock_stream: - mock_stream().endpoint_url.return_value = "http://home.assistant/playlist.m3u8" - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 2, - "type": "camera/stream", - "entity_id": "camera.my_camera", - } - ) - msg = await client.receive_json() + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "camera/stream", + "entity_id": "camera.my_camera", + } + ) + msg = await client.receive_json() assert msg["id"] == 2 assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"]["url"] == "http://home.assistant/playlist.m3u8" + assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM -async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): + +async def test_camera_ws_stream_failure( + hass, setup_platform, camera_device, hass_ws_client, auth +): """Test a basic camera that supports web rtc.""" auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -268,21 +316,22 @@ async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): assert msg["error"]["message"].startswith("Nest API error") -async def test_camera_stream_missing_trait(hass, auth): +async def test_camera_stream_missing_trait(hass, setup_platform, create_device): """Test fetching a video stream when not supported by the API.""" - traits = { - "sdm.devices.traits.Info": { - "customName": "My Camera", - }, - "sdm.devices.traits.CameraImage": { - "maxImageResolution": { - "width": 800, - "height": 600, - } - }, - } - - await async_setup_camera(hass, traits, auth=auth) + create_device.create( + { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 800, + "height": 600, + } + }, + } + ) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -292,12 +341,16 @@ async def test_camera_stream_missing_trait(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source is None - # Unable to get an image from the live stream - with pytest.raises(HomeAssistantError): - await async_get_image(hass) + # Fallback to placeholder image + await async_get_image(hass) -async def test_refresh_expired_stream_token(hass, auth): +async def test_refresh_expired_stream_token( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + camera_device: None, +): """Test a camera stream expiration and refresh.""" now = utcnow() stream_1_expiration = now + datetime.timedelta(seconds=90) @@ -311,11 +364,7 @@ async def test_refresh_expired_stream_token(hass, auth): # Stream URL #3 make_stream_url_response(stream_3_expiration, token_num=3), ] - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await setup_platform() assert await async_setup_component(hass, "stream", {}) assert len(hass.states.async_all()) == 1 @@ -372,7 +421,12 @@ async def test_refresh_expired_stream_token(hass, auth): assert hls_url == hls_url2 -async def test_stream_response_already_expired(hass, auth): +async def test_stream_response_already_expired( + hass: HomeAssistant, + auth: FakeAuth, + setup_platform: PlatformSetup, + camera_device: None, +): """Test a API response returning an expired stream url.""" now = utcnow() stream_1_expiration = now + datetime.timedelta(seconds=-90) @@ -381,7 +435,7 @@ async def test_stream_response_already_expired(hass, auth): make_stream_url_response(stream_1_expiration, token_num=1), make_stream_url_response(stream_2_expiration, token_num=2), ] - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -399,13 +453,15 @@ async def test_stream_response_already_expired(hass, auth): assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" -async def test_camera_removed(hass, auth): +async def test_camera_removed( + hass: HomeAssistant, + auth: FakeAuth, + camera_device: None, + subscriber: FakeSubscriber, + setup_platform: PlatformSetup, +): """Test case where entities are removed and stream tokens revoked.""" - subscriber = await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await setup_platform() # Simplify test setup subscriber.cache_policy.fetch = False @@ -422,29 +478,20 @@ async def test_camera_removed(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - # Fetch an event image, exercising cleanup on remove - await subscriber.async_receive_event(make_motion_event()) - await hass.async_block_till_done() - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_EVENT - for config_entry in hass.config_entries.async_entries(DOMAIN): await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -async def test_camera_remove_failure(hass, auth): +async def test_camera_remove_failure( + hass: HomeAssistant, + auth: FakeAuth, + camera_device: None, + setup_platform: PlatformSetup, +): """Test case where revoking the stream token fails on unload.""" - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -467,7 +514,12 @@ async def test_camera_remove_failure(hass, auth): assert len(hass.states.async_all()) == 0 -async def test_refresh_expired_stream_failure(hass, auth): +async def test_refresh_expired_stream_failure( + hass: HomeAssistant, + auth: FakeAuth, + setup_platform: PlatformSetup, + camera_device: None, +): """Tests a failure when refreshing the stream.""" now = utcnow() stream_1_expiration = now + datetime.timedelta(seconds=90) @@ -479,7 +531,7 @@ async def test_refresh_expired_stream_failure(hass, auth): # Next attempt to get a stream fetches a new url make_stream_url_response(expiration=stream_2_expiration, token_num=2), ] - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert await async_setup_component(hass, "stream", {}) assert len(hass.states.async_all()) == 1 @@ -517,161 +569,9 @@ async def test_refresh_expired_stream_failure(hass, auth): assert create_stream.called -async def test_camera_image_from_last_event(hass, auth): - """Test an image generated from an event.""" - # The subscriber receives a message related to an image event. The camera - # holds on to the event message. When the test asks for a capera snapshot - # it exchanges the event id for an image url and fetches the image. - subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - # Simulate a pubsub message received by the subscriber with a motion event. - await subscriber.async_receive_event(make_motion_event()) - await hass.async_block_till_done() - - auth.responses = [ - # Fake response from API that returns url image - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - # Fake response for the image content fetch - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_EVENT - # Verify expected image fetch request was captured - assert auth.url == TEST_IMAGE_URL - assert auth.headers == IMAGE_AUTHORIZATION_HEADERS - - # An additional fetch uses the cache and does not send another RPC - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_EVENT - # Verify expected image fetch request was captured - assert auth.url == TEST_IMAGE_URL - assert auth.headers == IMAGE_AUTHORIZATION_HEADERS - - -async def test_camera_image_from_event_not_supported(hass, auth): - """Test fallback to stream image when event images are not supported.""" - # Create a device that does not support the CameraEventImgae trait - traits = DEVICE_TRAITS.copy() - del traits["sdm.devices.traits.CameraEventImage"] - subscriber = await async_setup_camera(hass, traits, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - await subscriber.async_receive_event(make_motion_event()) - await hass.async_block_till_done() - - # Camera fetches a stream url since CameraEventImage is not supported - auth.responses = [make_stream_url_response()] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM - - -async def test_generate_event_image_url_failure(hass, auth): - """Test fallback to stream on failure to create an image url.""" - subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - await subscriber.async_receive_event(make_motion_event()) - await hass.async_block_till_done() - - auth.responses = [ - # Fail to generate the image url - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - # Camera fetches a stream url as a fallback - make_stream_url_response(), - ] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM - - -async def test_fetch_event_image_failure(hass, auth): - """Test fallback to a stream on image download failure.""" - subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - await subscriber.async_receive_event(make_motion_event()) - await hass.async_block_till_done() - - auth.responses = [ - # Fake response from API that returns url image - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - # Fail to download the image - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - # Camera fetches a stream url as a fallback - make_stream_url_response(), - ] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM - - -async def test_event_image_expired(hass, auth): - """Test fallback for an event event image that has expired.""" - subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - # Simulate a pubsub message has already expired - event_timestamp = utcnow() - datetime.timedelta(seconds=40) - await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) - await hass.async_block_till_done() - - # Fallback to a stream url since the event message is expired. - auth.responses = [make_stream_url_response()] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM - - -async def test_multiple_event_images(hass, auth): - """Test fallback for an event event image that has been cleaned up on expiration.""" - subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - # Simplify test setup - subscriber.cache_policy.fetch = False - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - event_timestamp = utcnow() - await subscriber.async_receive_event( - make_motion_event(event_session_id="event-session-1", timestamp=event_timestamp) - ) - await hass.async_block_till_done() - - auth.responses = [ - # Fake response from API that returns url image - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - # Fake response for the image content fetch - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - # Image is refetched after being cleared by expiration alarm - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=b"updated image bytes"), - ] - - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_EVENT - - next_event_timestamp = event_timestamp + datetime.timedelta(seconds=25) - await subscriber.async_receive_event( - make_motion_event( - event_id="updated-event-id", - event_session_id="event-session-2", - timestamp=next_event_timestamp, - ) - ) - await hass.async_block_till_done() - - image = await async_get_image(hass) - assert image.content == b"updated image bytes" - - -async def test_camera_web_rtc(hass, auth, hass_ws_client): +async def test_camera_web_rtc( + hass, auth, hass_ws_client, webrtc_camera_device, setup_platform +): """Test a basic camera that supports web rtc.""" expiration = utcnow() + datetime.timedelta(seconds=100) auth.responses = [ @@ -685,21 +585,7 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): } ) ] - device_traits = { - "sdm.devices.traits.Info": { - "customName": "My Camera", - }, - "sdm.devices.traits.CameraLiveStream": { - "maxVideoResolution": { - "width": 640, - "height": 480, - }, - "videoCodecs": ["H264"], - "audioCodecs": ["AAC"], - "supportedProtocols": ["WEB_RTC"], - }, - } - await async_setup_camera(hass, device_traits, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -724,15 +610,15 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" # Nest WebRTC cameras return a placeholder - content = await async_get_image(hass) - assert content.content_type == "image/jpeg" - content = await async_get_image(hass, width=1024, height=768) - assert content.content_type == "image/jpeg" + await async_get_image(hass) + await async_get_image(hass, width=1024, height=768) -async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): +async def test_camera_web_rtc_unsupported( + hass, auth, hass_ws_client, camera_device, setup_platform +): """Test a basic camera that supports web rtc.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -758,26 +644,14 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): assert msg["error"]["message"].startswith("Camera does not support WebRTC") -async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): +async def test_camera_web_rtc_offer_failure( + hass, auth, hass_ws_client, webrtc_camera_device, setup_platform +): """Test a basic camera that supports web rtc.""" auth.responses = [ aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST), ] - device_traits = { - "sdm.devices.traits.Info": { - "customName": "My Camera", - }, - "sdm.devices.traits.CameraLiveStream": { - "maxVideoResolution": { - "width": 640, - "height": 480, - }, - "videoCodecs": ["H264"], - "audioCodecs": ["AAC"], - "supportedProtocols": ["WEB_RTC"], - }, - } - await async_setup_camera(hass, device_traits, auth=auth) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -802,7 +676,9 @@ async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): assert msg["error"]["message"].startswith("Nest API error") -async def test_camera_multiple_streams(hass, auth, hass_ws_client): +async def test_camera_multiple_streams( + hass, auth, hass_ws_client, create_device, setup_platform, mock_create_stream +): """Test a camera supporting multiple stream types.""" expiration = utcnow() + datetime.timedelta(seconds=100) auth.responses = [ @@ -819,21 +695,23 @@ async def test_camera_multiple_streams(hass, auth, hass_ws_client): } ), ] - device_traits = { - "sdm.devices.traits.Info": { - "customName": "My Camera", - }, - "sdm.devices.traits.CameraLiveStream": { - "maxVideoResolution": { - "width": 640, - "height": 480, + create_device.create( + { + "sdm.devices.traits.Info": { + "customName": "My Camera", }, - "videoCodecs": ["H264"], - "audioCodecs": ["AAC"], - "supportedProtocols": ["WEB_RTC", "RTSP"], - }, - } - await async_setup_camera(hass, device_traits, auth=auth) + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC", "RTSP"], + }, + } + ) + await setup_platform() assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -846,8 +724,7 @@ async def test_camera_multiple_streams(hass, auth, hass_ws_client): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - image = await async_get_image(hass) - assert image.content == IMAGE_BYTES_FROM_STREAM + assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM # WebRTC stream client = await hass_ws_client(hass) diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 6b100969ea97a..5f3efa362b35b 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -677,6 +677,65 @@ async def test_thermostat_set_heat( } +async def test_thermostat_set_temperature_hvac_mode( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test setting HVAC mode while setting temperature.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + "sdm.devices.traits.ThermostatTemperatureSetpoint": { + "coolCelsius": 25.0, + }, + }, + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + + await common.async_set_temperature(hass, temperature=24.0, hvac_mode=HVAC_MODE_COOL) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool", + "params": {"coolCelsius": 24.0}, + } + + await common.async_set_temperature(hass, temperature=26.0, hvac_mode=HVAC_MODE_HEAT) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat", + "params": {"heatCelsius": 26.0}, + } + + await common.async_set_temperature( + hass, target_temp_low=20.0, target_temp_high=24.0, hvac_mode=HVAC_MODE_HEAT_COOL + ) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange", + "params": {"heatCelsius": 20.0, "coolCelsius": 24.0}, + } + + async def test_thermostat_set_heat_cool( hass: HomeAssistant, setup_platform: PlatformSetup, diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index ee286242a8ce2..0ab387a7dea33 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -28,7 +28,7 @@ EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." -EVENT_KEYS = {"device_id", "type", "timestamp"} +EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} def event_view(d: Mapping[str, Any]) -> Mapping[str, Any]: @@ -514,3 +514,37 @@ async def test_structure_update_event(hass): assert registry.async_get("camera.front") # Currently need a manual reload to detect the new entity assert not registry.async_get("camera.back") + + +async def test_event_zones(hass): + """Test events published with zone information.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits(["sdm.devices.traits.CameraMotion"]), + ) + registry = er.async_get(hass) + entry = registry.async_get("camera.front") + assert entry is not None + + event_map = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": EVENT_ID, + "zones": ["Zone 1"], + }, + } + + timestamp = utcnow() + await subscriber.async_receive_event(create_events(event_map, timestamp=timestamp)) + await hass.async_block_till_done() + + event_time = timestamp.replace(microsecond=0) + assert len(events) == 1 + assert event_view(events[0].data) == { + "device_id": entry.device_id, + "type": "camera_motion", + "timestamp": event_time, + "zones": ["Zone 1"], + } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 015a14fb92c45..2361049ecc1c6 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -90,6 +90,12 @@ def frame_image_data(frame_i, total_frames): return img +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + @pytest.fixture def mp4() -> io.BytesIO: """Generate test mp4 clip.""" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index b1cee88ee2ff0..2eaf713e8ee0c 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -364,7 +364,7 @@ async def fake_post(*args, **kwargs): await simulate_webhook(hass, webhook_id, response) await hass.async_block_till_done() - assert fake_post_hits == 5 + assert fake_post_hits == 8 calls = fake_post_hits @@ -446,7 +446,7 @@ async def fake_post_no_data(*args, **kwargs): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert fake_post_hits == 1 + assert fake_post_hits == 4 async def test_camera_image_raises_exception(hass, config_entry, requests_mock): @@ -491,4 +491,4 @@ async def fake_post(*args, **kwargs): await camera.async_get_image(hass, camera_entity_indoor) assert excinfo.value.args == ("Unable to get image",) - assert fake_post_hits == 6 + assert fake_post_hits == 9 diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index b97f4c8b4ec8b..30fb5fd3d47db 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -42,6 +42,7 @@ async def test_abort_if_existing_entry(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="0.0.0.0", + addresses=["0.0.0.0"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 950a45f1e4a1a..911fd6c309a23 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -112,7 +112,7 @@ async def fake_post(*args, **kwargs): await hass.async_block_till_done() - assert fake_post_hits == 3 + assert fake_post_hits == 9 mock_impl.assert_called_once() mock_webhook.assert_called_once() @@ -182,6 +182,8 @@ async def test_setup_with_cloud(hass, config_entry): with patch( "homeassistant.components.cloud.async_is_logged_in", return_value=True + ), patch( + "homeassistant.components.cloud.async_is_connected", return_value=True ), patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( @@ -203,6 +205,7 @@ async def test_setup_with_cloud(hass, config_entry): hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) assert hass.components.cloud.async_active_subscription() is True + assert hass.components.cloud.async_is_connected() is True fake_create_cloudhook.assert_called_once() assert ( @@ -245,6 +248,8 @@ async def test_setup_with_cloudhook(hass): with patch( "homeassistant.components.cloud.async_is_logged_in", return_value=True + ), patch( + "homeassistant.components.cloud.async_is_connected", return_value=True ), patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( @@ -354,7 +359,7 @@ async def test_setup_component_with_delay(hass, config_entry): await hass.async_block_till_done() - assert mock_post_request.call_count == 5 + assert mock_post_request.call_count == 8 mock_impl.assert_called_once() mock_webhook.assert_not_called() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index d28df01beccdf..a0992e7ea2c89 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -11,6 +11,8 @@ from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhook +from tests.test_util.aiohttp import AiohttpClientMockResponse + async def test_light_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" @@ -89,7 +91,11 @@ async def fake_post_request_no_data(*args, **kwargs): """Fake error during requesting backend data.""" nonlocal fake_post_hits fake_post_hits += 1 - return "{}" + return AiohttpClientMockResponse( + method="POST", + url=kwargs["url"], + json={}, + ) with patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" @@ -115,7 +121,7 @@ async def fake_post_request_no_data(*args, **kwargs): ) await hass.async_block_till_done() - assert fake_post_hits == 1 + assert fake_post_hits == 4 assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) == 0 diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 2ba70ca9489fa..db1a79145b4bc 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -52,7 +52,7 @@ async def test_async_browse_media(hass): assert str(excinfo.value) == "Unknown source directory." # Test invalid base - with pytest.raises(ValueError) as excinfo: + with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") assert str(excinfo.value) == "Invalid media source URI" diff --git a/tests/components/nina/__init__.py b/tests/components/nina/__init__.py index 92697378293bd..d6c9fffdfa786 100644 --- a/tests/components/nina/__init__.py +++ b/tests/components/nina/__init__.py @@ -1 +1,25 @@ """Tests for the Nina integration.""" +import json +from typing import Any + +from tests.common import load_fixture + + +def mocked_request_function(url: str) -> dict[str, Any]: + """Mock of the request function.""" + dummy_response: dict[str, Any] = json.loads( + load_fixture("sample_warnings.json", "nina") + ) + + dummy_response_details: dict[str, Any] = json.loads( + load_fixture("sample_warning_details.json", "nina") + ) + + if url == "https://warnung.bund.de/api31/dashboard/083350000000.json": + return dummy_response + + warning_id = url.replace("https://warnung.bund.de/api31/warnings/", "").replace( + ".json", "" + ) + + return dummy_response_details[warning_id] diff --git a/tests/components/nina/fixtures/sample_warning_details.json b/tests/components/nina/fixtures/sample_warning_details.json new file mode 100644 index 0000000000000..61c28dc9992a3 --- /dev/null +++ b/tests/components/nina/fixtures/sample_warning_details.json @@ -0,0 +1,167 @@ +{ + "mow.DE-BW-S-SE018-20211102-18-001": { + "identifier": "mow.DE-BW-S-SE018-20211102-18-001", + "sender": "DE-NW-BN-SE030", + "sent": "2021-11-02T20:07:16+01:00", + "status": "Actual", + "msgType": "Update", + "scope": "Public", + "code": [ + "DVN:1", + "medien_ueberregional", + "nina", + "Materna:noPush", + "Materna:noMirror" + ], + "references": "DE-NW-BN-SE030-20200506-30-001 DE-NW-BN-SE030-20200422-30-000 DE-NW-BN-SE030-20200420-30-001 DE-NW-BN-SE030-20200416-30-001 DE-NW-BN-SE030-20200403-30-000 DE-NW-BN-W003,mow.DE-NW-BN-SE030-20200506-30-001 mow.DE-NW-BN-SE030-20200422-30-000 mow.DE-NW-BN-SE030-20200420-30-001 mow.DE-NW-BN-SE030-20200416-30-001 mow.DE-NW-BN-SE030-20200403-30-000 mow.DE-NW-BN-W003-20200403-000,2020-04-03T00:00:00+00:00", + "info": [ + { + "language": "DE", + "category": [ + "Health" + ], + "event": "Gefahreninformation", + "urgency": "Immediate", + "severity": "Minor", + "certainty": "Observed", + "eventCode": [ + { + "valueName": "profile:DE-BBK-EVENTCODE", + "value": "BBK-EVC-040" + } + ], + "headline": "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen", + "description": "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen.", + "instruction": "Waschen Sie sich regelmäßig und gründlich die Hände.
- Beachten Sie die AHA + A + L - Regeln:
Abstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden!
Hygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten!
Alltagsmaske (Mund-Nase-Bedeckung) tragen!
App - installieren und nutzen Sie die Corona-Warn-App!
Lüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit!
- Bitte folgen Sie den behördlichen Anordnungen.
- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge.
- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus.
- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen.", + "contact": "Weitere Informationen und Empfehlungen finden Sie im Corona-Informations-Bereich der Warn-App NINA. Beachten Sie auch die Internetseiten der örtlichen Gesundheitsbehörde (Stadt- bzw. Kreisverwaltung) Ihres Aufenthaltsortes", + "parameter": [ + { + "valueName": "instructionText", + "value": "- Beachten Sie die AHA + A + L - Regeln:\nAbstand halten - 1,5 m Mindestabstand beachten, Körperkontakt vermeiden! \nHygiene - regelmäßiges Händewaschen, Husten- und Nieshygiene beachten! \nAlltagsmaske (Mund-Nase-Bedeckung) tragen! \nApp - installieren und nutzen Sie die Corona-Warn-App! \nLüften: Sorgen Sie für eine regelmäßige und gründliche Lüftung von Räumen - auch und gerade in der kommenden kalten Jahreszeit! \n- Bitte folgen Sie den behördlichen Anordnungen. \n- Husten und niesen Sie in ein Taschentuch oder in die Armbeuge. \n- Bleiben Sie bei Erkältungssymptomen nach Möglichkeit zu Hause. Kontaktieren Sie Ihre Hausarztpraxis per Telefon oder wenden sich an die Telefonnummer 116117 des Ärztlichen Bereitschaftsdienstes und besprechen Sie das weitere Vorgehen. Gehen Sie nicht unaufgefordert in eine Arztpraxis oder ins Krankenhaus. \n- Seien Sie kritisch: Informieren Sie sich nur aus gesicherten Quellen." + }, + { + "valueName": "warnVerwaltungsbereiche", + "value": "130000000000,140000000000,160000000000,110000000000,020000000000,070000000000,030000000000,050000000000,080000000000,120000000000,010000000000,150000000000,040000000000,060000000000,090000000000,100000000000" + }, + { + "valueName": "instructionCode", + "value": "BBK-ISC-132" + }, + { + "valueName": "sender_langname", + "value": "BBK, Nationale Warnzentrale Bonn" + }, + { + "valueName": "sender_signature", + "value": "Bundesamt für Bevölkerungsschutz und Katastrophenhilfe\nNationale Warnzentrale Bonn\nhttps://warnung.bund.de" + }, + { + "valueName": "PHGEM", + "value": "1+11057,100001" + }, + { + "valueName": "ZGEM", + "value": "1+11057,100001" + } + ], + "area": [ + { + "areaDesc": "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg", + "geocode": [ + { + "valueName": "AreaId", + "value": "0" + } + ] + } + ] + } + ] + }, + "mow.DE-NW-BN-SE030-20201014-30-000" : { + "identifier": "mow.DE-NW-BN-SE030-20201014-30-000", + "sender": "opendata@dwd.de", + "sent": "2021-10-11T05:20:00+01:00", + "status": "Actual", + "msgType": "Alert", + "source": "PVW", + "scope": "Public", + "code": [ + "DVN:2", + "id:2.49.0.0.276.0.DWD.PVW.1645004040000.5a168da8-ac20-4b6d-86be-d616526a7914" + ], + "info": [ + { + "language": "de-DE", + "category": [ + "Met" + ], + "event": "STURMBÖEN", + "responseType": [ + "Prepare" + ], + "urgency": "Immediate", + "severity": "Moderate", + "certainty": "Likely", + "eventCode": [ + { + "valueName": "PROFILE_VERSION", + "value": "2.1.11" + }, + { + "valueName": "LICENSE", + "value": "© GeoBasis-DE / BKG 2019 (Daten modifiziert)" + }, + { + "valueName": "II", + "value": "52" + }, + { + "valueName": "GROUP", + "value": "WIND" + }, + { + "valueName": "AREA_COLOR", + "value": "251 140 0" + } + ], + "effective": "2021-11-01T03:20:00+01:00", + "onset": "2021-11-01T05:20:00+01:00", + "expires": "3021-11-22T05:19:00+01:00", + "senderName": "Deutscher Wetterdienst", + "headline": "Ausfall Notruf 112", + "description": "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.", + "instruction": "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "web": "https://www.wettergefahren.de", + "contact": "Deutscher Wetterdienst", + "parameter": [ + { + "valueName": "gusts", + "value": "70-85 [km/h]" + }, + { + "valueName": "exposed gusts", + "value": "<90 [km/h]" + }, + { + "valueName": "wind direction", + "value": "west" + }, + { + "valueName": "PHGEM", + "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" + }, + { + "valueName": "ZGEM", + "value": "3243+168,3413+1,3424+52,3478+1,3495+2,3499,3639+2527,6168+1,6175+22,6199+36,6238,6241+7,6256,9956+184,10142,10154,10164+7,10173,10176+6,10186+1,10195+2,10199,10201+6,10214+4,10220,10249+117,10368,10373+2,10425+9,10436+1,10440+8,10450+1,10453+7,10462+1,10467+5,10474+2,10484+5,10773+68,10843+2,10847+9,10858,10867+8,10878+1,10882+68,10952+7,10961+2,11046,11056+1" + } + ], + "area": [ + { + "areaDesc": "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/components/nina/fixtures/sample_warnings.json b/tests/components/nina/fixtures/sample_warnings.json index d53fecffa6320..b49e436ef8b56 100644 --- a/tests/components/nina/fixtures/sample_warnings.json +++ b/tests/components/nina/fixtures/sample_warnings.json @@ -37,7 +37,7 @@ } }, "i18nTitle": {"de": "Ausfall Notruf 112"}, - "start": "2021-11-01T05:20:00+01:00", + "onset": "2021-11-01T05:20:00+01:00", "sent": "2021-10-11T05:20:00+01:00", "expires": "3021-11-22T05:19:00+01:00" } diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index ebdd7ed4105ea..d0a65e190f0f1 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -1,5 +1,4 @@ """Test the Nina binary sensor.""" -import json from typing import Any from unittest.mock import patch @@ -17,7 +16,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry, load_fixture +from . import mocked_request_function + +from tests.common import MockConfigEntry ENTRY_DATA: dict[str, Any] = { "slots": 5, @@ -35,13 +36,9 @@ async def test_sensors(hass: HomeAssistant) -> None: """Test the creation and values of the NINA sensors.""" - dummy_response: dict[str, Any] = json.loads( - load_fixture("sample_warnings.json", "nina") - ) - with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=dummy_response, + wraps=mocked_request_function, ): conf_entry: MockConfigEntry = MockConfigEntry( @@ -125,13 +122,9 @@ async def test_sensors(hass: HomeAssistant) -> None: async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: """Test the creation and values of the NINA sensors without the corona filter.""" - dummy_response: dict[str, Any] = json.loads( - load_fixture("nina/sample_warnings.json") - ) - with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=dummy_response, + wraps=mocked_request_function, ): conf_entry: MockConfigEntry = MockConfigEntry( diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 455d7465a8776..66e8edb280675 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -1,5 +1,4 @@ """Test the Nina init file.""" -import json from typing import Any from unittest.mock import patch @@ -10,7 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from . import mocked_request_function + +from tests.common import MockConfigEntry ENTRY_DATA: dict[str, Any] = { "slots": 5, @@ -22,13 +23,9 @@ async def init_integration(hass) -> MockConfigEntry: """Set up the NINA integration in Home Assistant.""" - dummy_response: dict[str, Any] = json.loads( - load_fixture("sample_warnings.json", "nina") - ) - with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=dummy_response, + wraps=mocked_request_function, ): entry: MockConfigEntry = MockConfigEntry( diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 733d5807c5f63..2261fec3a8614 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -37,6 +37,7 @@ async def test_form_zeroconf(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.5", + addresses=["192.168.1.5"], hostname="mock_hostname", name="mock_name", port=1234, diff --git a/tests/components/octoprint/test_button.py b/tests/components/octoprint/test_button.py new file mode 100644 index 0000000000000..603739159afb0 --- /dev/null +++ b/tests/components/octoprint/test_button.py @@ -0,0 +1,195 @@ +"""Test the OctoPrint buttons.""" +from unittest.mock import patch + +from pyoctoprintapi import OctoprintPrinterInfo +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.octoprint import OctoprintDataUpdateCoordinator +from homeassistant.components.octoprint.button import InvalidPrinterState +from homeassistant.components.octoprint.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import init_integration + + +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" + ] + + # Test pausing the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": True}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_pause_job", + }, + blocking=True, + ) + + assert len(pause_command.mock_calls) == 1 + + # Test pausing the printer when it is paused + with patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_pause_job", + }, + blocking=True, + ) + + assert len(pause_command.mock_calls) == 0 + + # Test pausing the printer when it is stopped + with patch( + "pyoctoprintapi.OctoprintClient.pause_job" + ) as pause_command, pytest.raises(InvalidPrinterState): + corrdinator.data["printer"] = OctoprintPrinterInfo( + { + "state": {"flags": {"printing": False, "paused": False}}, + "temperature": [], + } + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_pause_job", + }, + blocking=True, + ) + + +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" + ] + + # Test resuming the printer when it is paused + with patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_resume_job", + }, + blocking=True, + ) + + assert len(resume_command.mock_calls) == 1 + + # Test resuming the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": True, "paused": False}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_resume_job", + }, + blocking=True, + ) + + assert len(resume_command.mock_calls) == 0 + + # Test resuming the printer when it is stopped + with patch( + "pyoctoprintapi.OctoprintClient.resume_job" + ) as resume_command, pytest.raises(InvalidPrinterState): + corrdinator.data["printer"] = OctoprintPrinterInfo( + { + "state": {"flags": {"printing": False, "paused": False}}, + "temperature": [], + } + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_resume_job", + }, + blocking=True, + ) + + +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" + ] + + # Test stopping the printer when it is paused + with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": False, "paused": True}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_stop_job", + }, + blocking=True, + ) + + assert len(stop_command.mock_calls) == 1 + + # Test stopping the printer when it is printing + with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + {"state": {"flags": {"printing": True, "paused": False}}, "temperature": []} + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_stop_job", + }, + blocking=True, + ) + + assert len(stop_command.mock_calls) == 1 + + # Test stopping the printer when it is stopped + with patch("pyoctoprintapi.OctoprintClient.cancel_job") as stop_command: + corrdinator.data["printer"] = OctoprintPrinterInfo( + { + "state": {"flags": {"printing": False, "paused": False}}, + "temperature": [], + } + ) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.octoprint_stop_job", + }, + blocking=True, + ) + + assert len(stop_command.mock_calls) == 0 diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 422b47668aa46..7769e0820161c 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -175,6 +175,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=80, @@ -496,6 +497,7 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=80, diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 9605fb9e71cd4..976e2b84c68eb 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -381,6 +381,23 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): assert len(hass.states.async_entity_ids("weather")) == 1 +async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_client): + """Test finishing the core step set up the radio browser.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + client = await hass_client() + + resp = await client.post("/api/onboarding/core_config") + + assert resp.status == 200 + + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries("radio_browser")) == 1 + + async def test_onboarding_core_sets_up_rpi_power( hass, hass_storage, hass_client, aioclient_mock, rpi ): diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 336dafb15a1fc..32212f84b34fc 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -4,6 +4,7 @@ import pytest +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -83,7 +84,7 @@ async def test_owserver_switch( expected_entity[ATTR_STATE] = STATE_ON await hass.services.async_call( - Platform.SWITCH, + SWITCH_DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True, diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index e80add482b0d3..db86a4abc5c64 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -33,6 +33,7 @@ FAKE_ZERO_CONF_INFO = ZeroconfServiceInfo( host="192.168.0.51", + addresses=["192.168.0.51"], port=443, hostname=f"gateway-{TEST_GATEWAY_ID}.local.", type="_kizbox._tcp.local.", diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index b6bcc17a03d22..3ea54cee59371 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -1,23 +1,20 @@ """Test the Picnic config flow.""" from unittest.mock import patch +import pytest from python_picnic_api.session import PicnicAuthError import requests -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN +from tests.common import MockConfigEntry -async def test_form(hass): - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] is None +@pytest.fixture +def picnic_api(): + """Create PicnicAPI mock with set response data.""" auth_token = "af3wh738j3fa28l9fa23lhiufahu7l" auth_data = { "user_id": "f29-2a6-o32n", @@ -29,13 +26,27 @@ async def test_form(hass): } with patch( "homeassistant.components.picnic.config_flow.PicnicAPI", - ) as mock_picnic, patch( + ) as picnic_mock: + picnic_mock().session.auth_token = auth_token + picnic_mock().get_user.return_value = auth_data + + yield picnic_mock + + +async def test_form(hass, picnic_api): + """Test we get the form and a config entry is created.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] is None + + with patch( "homeassistant.components.picnic.async_setup_entry", return_value=True, ) as mock_setup_entry: - mock_picnic().session.auth_token = auth_token - mock_picnic().get_user.return_value = auth_data - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -49,14 +60,14 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Teststreet 123b" assert result2["data"] == { - CONF_ACCESS_TOKEN: auth_token, + CONF_ACCESS_TOKEN: picnic_api().session.auth_token, CONF_COUNTRY_CODE: "NL", } assert len(mock_setup_entry.mock_calls) == 1 async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" + """Test we handle invalid authentication.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -74,12 +85,12 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" + """Test we handle connection errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -97,7 +108,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -120,5 +131,150 @@ async def test_form_exception(hass): }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass, picnic_api): + """Test that an entry with unique id can only be added once.""" + # Create a mocked config entry and make sure to use the same user_id as set for the picnic_api mock response. + MockConfigEntry( + domain=DOMAIN, + unique_id=picnic_api().get_user()["user_id"], + data={CONF_ACCESS_TOKEN: "a3p98fsen.a39p3fap", CONF_COUNTRY_CODE: "NL"}, + ).add_to_hass(hass) + + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result_configure = await hass.config_entries.flow.async_configure( + result_init["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + await hass.async_block_till_done() + + assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["reason"] == "already_configured" + + +async def test_step_reauth(hass, picnic_api): + """Test the re-auth flow.""" + # Create a mocked config entry + conf = {CONF_ACCESS_TOKEN: "a3p98fsen.a39p3fap", CONF_COUNTRY_CODE: "NL"} + + MockConfigEntry( + domain=DOMAIN, + unique_id=picnic_api().get_user()["user_id"], + data=conf, + ).add_to_hass(hass) + + # Init a re-auth flow + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf + ) + assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["step_id"] == "user" + + with patch( + "homeassistant.components.picnic.async_setup_entry", + return_value=True, + ): + result_configure = await hass.config_entries.flow.async_configure( + result_init["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + await hass.async_block_till_done() + + # Check that the returned flow has type abort because of successful re-authentication + assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_step_reauth_failed(hass): + """Test the re-auth flow when authentication fails.""" + # Create a mocked config entry + user_id = "f29-2a6-o32n" + conf = {CONF_ACCESS_TOKEN: "a3p98fsen.a39p3fap", CONF_COUNTRY_CODE: "NL"} + + MockConfigEntry( + domain=DOMAIN, + unique_id=user_id, + data=conf, + ).add_to_hass(hass) + + # Init a re-auth flow + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf + ) + assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["step_id"] == "user" + + with patch( + "homeassistant.components.picnic.config_flow.PicnicHub.authenticate", + side_effect=PicnicAuthError, + ): + result_configure = await hass.config_entries.flow.async_configure( + result_init["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + await hass.async_block_till_done() + + # Check that the returned flow has type form with error set + assert result_configure["type"] == "form" + assert result_configure["errors"] == {"base": "invalid_auth"} + + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_step_reauth_different_account(hass, picnic_api): + """Test the re-auth flow when authentication is done with a different account.""" + # Create a mocked config entry, unique_id should be different that the user id in the api response + conf = {CONF_ACCESS_TOKEN: "a3p98fsen.a39p3fap", CONF_COUNTRY_CODE: "NL"} + + MockConfigEntry( + domain=DOMAIN, + unique_id="3fpawh-ues-af3ho", + data=conf, + ).add_to_hass(hass) + + # Init a re-auth flow + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf + ) + assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["step_id"] == "user" + + with patch( + "homeassistant.components.picnic.async_setup_entry", + return_value=True, + ): + result_configure = await hass.config_entries.flow.async_configure( + result_init["flow_id"], + { + "username": "test-username", + "password": "test-password", + "country_code": "NL", + }, + ) + await hass.async_block_till_done() + + # Check that the returned flow has type form with error set + assert result_configure["type"] == "form" + assert result_configure["errors"] == {"base": "different_account"} + + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index a4a52e50453eb..7b1bdeb1d129d 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -239,27 +239,37 @@ async def test_sensors_setup(self): ) self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED") self._assert_sensor( - "sensor.picnic_last_order_eta_start", - "2021-02-26T19:54:00+00:00", + "sensor.picnic_last_order_max_order_time", + "2021-02-25T21:00:00+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", - "2021-02-26T20:14:00+00:00", + "sensor.picnic_last_order_delivery_time", + "2021-02-26T19:54:05+00:00", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_max_order_time", - "2021-02-25T21:00:00+00:00", + "sensor.picnic_last_order_total_price", "41.33", unit=CURRENCY_EURO + ) + self._assert_sensor( + "sensor.picnic_next_delivery_eta_start", + "unknown", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_delivery_time", - "2021-02-26T19:54:05+00:00", + "sensor.picnic_next_delivery_eta_end", + "unknown", cls=SensorDeviceClass.TIMESTAMP, ) self._assert_sensor( - "sensor.picnic_last_order_total_price", "41.33", unit=CURRENCY_EURO + "sensor.picnic_next_delivery_slot_start", + "unknown", + cls=SensorDeviceClass.TIMESTAMP, + ) + self._assert_sensor( + "sensor.picnic_next_delivery_slot_end", + "unknown", + cls=SensorDeviceClass.TIMESTAMP, ) async def test_sensors_setup_disabled_by_default(self): @@ -271,6 +281,8 @@ async def test_sensors_setup_disabled_by_default(self): self._assert_sensor("sensor.picnic_last_order_slot_end", disabled=True) self._assert_sensor("sensor.picnic_last_order_status", disabled=True) self._assert_sensor("sensor.picnic_last_order_total_price", disabled=True) + self._assert_sensor("sensor.picnic_next_delivery_slot_start", disabled=True) + self._assert_sensor("sensor.picnic_next_delivery_slot_end", disabled=True) async def test_sensors_no_selected_time_slot(self): """Test sensor states with no explicit selected time slot.""" @@ -295,11 +307,12 @@ async def test_sensors_no_selected_time_slot(self): "sensor.picnic_selected_slot_min_order_value", STATE_UNKNOWN ) - async def test_sensors_last_order_in_future(self): + async def test_next_delivery_sensors(self): """Test sensor states when last order is not yet delivered.""" # Adjust default delivery response delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) del delivery_response["delivery_time"] + delivery_response["status"] = "CURRENT" # Set mock responses self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE) @@ -311,10 +324,16 @@ async def test_sensors_last_order_in_future(self): # Assert delivery time is not available, but eta is self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-02-26T19:54:00+00:00" + "sensor.picnic_next_delivery_eta_start", "2021-02-26T19:54:00+00:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-02-26T20:14:00+00:00" + "sensor.picnic_next_delivery_eta_end", "2021-02-26T20:14:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_slot_start", "2021-02-26T19:15:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_slot_end", "2021-02-26T20:15:00+00:00" ) async def test_sensors_eta_date_malformed(self): @@ -329,12 +348,13 @@ async def test_sensors_eta_date_malformed(self): } delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) delivery_response["eta2"] = eta_dates + delivery_response["status"] = "CURRENT" self.picnic_mock().get_deliveries.return_value = [delivery_response] await self._coordinator.async_refresh() # Assert eta times are not available due to malformed date strings - self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNKNOWN) async def test_sensors_use_detailed_eta_if_available(self): """Test sensor states when last order is not yet delivered.""" @@ -344,6 +364,7 @@ async def test_sensors_use_detailed_eta_if_available(self): # Provide a delivery position response with different ETA and remove delivery time from response delivery_response = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) del delivery_response["delivery_time"] + delivery_response["status"] = "CURRENT" self.picnic_mock().get_deliveries.return_value = [delivery_response] self.picnic_mock().get_delivery_position.return_value = { "eta_window": { @@ -358,10 +379,10 @@ async def test_sensors_use_detailed_eta_if_available(self): delivery_response["delivery_id"] ) self._assert_sensor( - "sensor.picnic_last_order_eta_start", "2021-03-05T10:19:20+00:00" + "sensor.picnic_next_delivery_eta_start", "2021-03-05T10:19:20+00:00" ) self._assert_sensor( - "sensor.picnic_last_order_eta_end", "2021-03-05T10:39:20+00:00" + "sensor.picnic_next_delivery_eta_end", "2021-03-05T10:39:20+00:00" ) async def test_sensors_no_data(self): @@ -387,12 +408,12 @@ async def test_sensors_no_data(self): self._assert_sensor( "sensor.picnic_selected_slot_min_order_value", STATE_UNAVAILABLE ) - self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNAVAILABLE) - self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNAVAILABLE) self._assert_sensor( "sensor.picnic_last_order_max_order_time", STATE_UNAVAILABLE ) self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNAVAILABLE) + self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNAVAILABLE) async def test_sensors_malformed_delivery_data(self): """Test sensor states when the delivery api returns not a list.""" @@ -405,10 +426,10 @@ async def test_sensors_malformed_delivery_data(self): # Assert all last-order sensors have STATE_UNAVAILABLE because the delivery info fetch failed assert self._coordinator.last_update_success is True - self._assert_sensor("sensor.picnic_last_order_eta_start", STATE_UNKNOWN) - self._assert_sensor("sensor.picnic_last_order_eta_end", STATE_UNKNOWN) self._assert_sensor("sensor.picnic_last_order_max_order_time", STATE_UNKNOWN) self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_next_delivery_eta_start", STATE_UNKNOWN) + self._assert_sensor("sensor.picnic_next_delivery_eta_end", STATE_UNKNOWN) async def test_sensors_malformed_response(self): """Test coordinator update fails when API yields ValueError.""" @@ -423,6 +444,55 @@ async def test_sensors_malformed_response(self): # Assert coordinator update failed assert self._coordinator.last_update_success is False + async def test_multiple_active_orders(self): + """Test that the sensors get the right values when there are multiple active orders.""" + # Create 2 undelivered orders + undelivered_order = copy.deepcopy(DEFAULT_DELIVERY_RESPONSE) + del undelivered_order["delivery_time"] + undelivered_order["status"] = "CURRENT" + undelivered_order["slot"]["window_start"] = "2022-03-01T09:15:00.000+01:00" + undelivered_order["slot"]["window_end"] = "2022-03-01T10:15:00.000+01:00" + undelivered_order["eta2"]["start"] = "2022-03-01T09:30:00.000+01:00" + undelivered_order["eta2"]["end"] = "2022-03-01T09:45:00.000+01:00" + + undelivered_order_2 = copy.deepcopy(undelivered_order) + undelivered_order_2["slot"]["window_start"] = "2022-03-08T13:15:00.000+01:00" + undelivered_order_2["slot"]["window_end"] = "2022-03-08T14:15:00.000+01:00" + undelivered_order_2["eta2"]["start"] = "2022-03-08T13:30:00.000+01:00" + undelivered_order_2["eta2"]["end"] = "2022-03-08T13:45:00.000+01:00" + + deliveries_response = [ + undelivered_order_2, + undelivered_order, + copy.deepcopy(DEFAULT_DELIVERY_RESPONSE), + ] + + # Set mock responses + self.picnic_mock().get_user.return_value = copy.deepcopy(DEFAULT_USER_RESPONSE) + self.picnic_mock().get_cart.return_value = copy.deepcopy(DEFAULT_CART_RESPONSE) + self.picnic_mock().get_deliveries.return_value = deliveries_response + self.picnic_mock().get_delivery_position.return_value = {} + await self._setup_platform() + + self._assert_sensor( + "sensor.picnic_last_order_slot_start", "2022-03-08T12:15:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_last_order_slot_end", "2022-03-08T13:15:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_slot_start", "2022-03-01T08:15:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_slot_end", "2022-03-01T09:15:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_eta_start", "2022-03-01T08:30:00+00:00" + ) + self._assert_sensor( + "sensor.picnic_next_delivery_eta_end", "2022-03-01T08:45:00+00:00" + ) + async def test_device_registry_entry(self): """Test if device registry entry is populated correctly.""" # Setup platform and default mock responses diff --git a/tests/components/plex/test_button.py b/tests/components/plex/test_button.py new file mode 100644 index 0000000000000..b540ba2d03165 --- /dev/null +++ b/tests/components/plex/test_button.py @@ -0,0 +1,36 @@ +"""Tests for Plex buttons.""" +from datetime import timedelta +from unittest.mock import patch + +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.plex.const import DEBOUNCE_TIMEOUT +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_scan_clients_button_schedule(hass, setup_plex_server): + """Test scan_clients button scheduled update.""" + with patch( + "homeassistant.components.plex.server.PlexServer._async_update_platforms" + ) as mock_scan_clients: + await setup_plex_server() + mock_scan_clients.reset_mock() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT), + ) + + assert await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.scan_clients_plex_server_1", + }, + True, + ) + await hass.async_block_till_done() + + assert mock_scan_clients.called diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index fced4bae58ab3..8e09f7023862e 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -276,3 +276,20 @@ async def test_bad_token_with_tokenless_server( # Ensure updates that rely on account return nothing trigger_plex_update(mock_websocket) await hass.async_block_till_done() + + +async def test_scan_clients_schedule(hass, setup_plex_server): + """Test scan_clients scheduled update.""" + with patch( + "homeassistant.components.plex.server.PlexServer._async_update_platforms" + ) as mock_scan_clients: + await setup_plex_server() + mock_scan_clients.reset_mock() + + async_fire_time_changed( + hass, + dt_util.utcnow() + const.CLIENT_SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + assert mock_scan_clients.called diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index adfdff2d1dcbe..f73fdea2806a9 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MEDIA_PLAYER_DOMAIN, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -15,7 +16,7 @@ SERVICE_PLAY_MEDIA, ) from homeassistant.components.plex.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError @@ -29,7 +30,7 @@ async def test_media_lookups( requests_mock.get("/player/playback/playMedia", status_code=200) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -41,7 +42,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -56,7 +57,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -69,7 +70,7 @@ async def test_media_lookups( with patch("plexapi.library.LibrarySection.search") as search: assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -81,7 +82,7 @@ async def test_media_lookups( search.assert_called_with(**{"show.title": "TV Show", "libtype": "show"}) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -95,7 +96,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -109,7 +110,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -128,7 +129,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -140,7 +141,7 @@ async def test_media_lookups( search.assert_called_with(**{"artist.title": "Artist", "libtype": "artist"}) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -152,7 +153,7 @@ async def test_media_lookups( search.assert_called_with(**{"album.title": "Album", "libtype": "album"}) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -166,7 +167,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -180,7 +181,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -199,7 +200,7 @@ async def test_media_lookups( ) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -219,7 +220,7 @@ async def test_media_lookups( # Movie searches assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -231,7 +232,7 @@ async def test_media_lookups( search.assert_called_with(**{"movie.title": "Movie 1", "libtype": None}) assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -247,7 +248,7 @@ async def test_media_lookups( payload = '{"library_name": "Movies", "title": "Not a Movie"}' with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -261,7 +262,7 @@ async def test_media_lookups( # Playlist searches assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -274,7 +275,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = '{"playlist_name": "Not a Playlist"}' assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, @@ -289,7 +290,7 @@ async def test_media_lookups( with pytest.raises(HomeAssistantError) as excinfo: payload = "{}" assert await hass.services.async_call( - Platform.MEDIA_PLAYER, + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player_id, diff --git a/tests/components/plugwise/common.py b/tests/components/plugwise/common.py deleted file mode 100644 index 379929ce2f150..0000000000000 --- a/tests/components/plugwise/common.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Common initialisation for the Plugwise integration.""" - -from homeassistant.components.plugwise.const import DOMAIN -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def async_init_integration( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - skip_setup: bool = False, -): - """Initialize the Smile integration.""" - - entry = MockConfigEntry( - domain=DOMAIN, data={"host": "1.1.1.1", "password": "test-password"} - ) - entry.add_to_hass(hass) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 06f9b56e6894e..012734315f687 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -1,225 +1,176 @@ """Setup mocks for the Plugwise integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +import json +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch -from functools import partial -from http import HTTPStatus -import re -from unittest.mock import AsyncMock, Mock, patch - -import jsonpickle -from plugwise.exceptions import ( - ConnectionFailedError, - InvalidAuthentication, - PlugwiseException, - XMLDataMissingError, -) import pytest -from tests.common import load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker +from homeassistant.components.plugwise.const import API, DOMAIN, PW_TYPE +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry, load_fixture -def _read_json(environment, call): + +def _read_json(environment: str, call: str) -> dict[str, Any]: """Undecode the json data.""" fixture = load_fixture(f"plugwise/{environment}/{call}.json") - return jsonpickle.decode(fixture) + return json.loads(fixture) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My Plugwise", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_PASSWORD: "test-password", + CONF_PORT: 80, + CONF_USERNAME: "smile", + PW_TYPE: API, + }, + unique_id="smile98765", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.plugwise.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup -@pytest.fixture(name="mock_smile") -def mock_smile(): - """Create a Mock Smile for testing exceptions.""" +@pytest.fixture() +def mock_smile_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Smile client.""" with patch( "homeassistant.components.plugwise.config_flow.Smile", + autospec=True, ) as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.return_value.connect.return_value = True - yield smile_mock.return_value + smile = smile_mock.return_value + smile.smile_hostname = "smile12345" + smile.smile_name = "Test Smile Name" + smile.connect.return_value = True + yield smile -@pytest.fixture(name="mock_smile_unauth") -def mock_smile_unauth(aioclient_mock: AiohttpClientMocker) -> None: - """Mock the Plugwise Smile unauthorized for Home Assistant.""" - aioclient_mock.get(re.compile(".*"), status=HTTPStatus.UNAUTHORIZED) - aioclient_mock.put(re.compile(".*"), status=HTTPStatus.UNAUTHORIZED) - +@pytest.fixture +def mock_smile_adam() -> Generator[None, MagicMock, None]: + """Create a Mock Adam environment for testing exceptions.""" + chosen_env = "adam_multiple_devices_per_zone" -@pytest.fixture(name="mock_smile_error") -def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None: - """Mock the Plugwise Smile server failure for Home Assistant.""" - aioclient_mock.get(re.compile(".*"), status=HTTPStatus.INTERNAL_SERVER_ERROR) - aioclient_mock.put(re.compile(".*"), status=HTTPStatus.INTERNAL_SERVER_ERROR) + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + smile.gateway_id = "fe799307f1624099878210aa0b9f1475" + smile.heater_id = "90986d591dcd426cae3ec3e8111ff730" + smile.smile_version = "3.0.15" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Adam" -@pytest.fixture(name="mock_smile_notconnect") -def mock_smile_notconnect(): - """Mock the Plugwise Smile general connection failure for Home Assistant.""" - with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.PlugwiseException = PlugwiseException - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False) - yield smile_mock.return_value + smile.connect.return_value = True + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") -def _get_device_data(chosen_env, device_id): - """Mock return data for specific devices.""" - return _read_json(chosen_env, "get_device_data/" + device_id) + yield smile -@pytest.fixture(name="mock_smile_adam") -def mock_smile_adam(): - """Create a Mock Adam environment for testing exceptions.""" - chosen_env = "adam_multiple_devices_per_zone" - with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.XMLDataMissingError = XMLDataMissingError - - smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475" - smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" - smile_mock.return_value.smile_version = "3.0.15" - smile_mock.return_value.smile_type = "thermostat" - smile_mock.return_value.smile_hostname = "smile98765" - - smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") - - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.full_update_device.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.single_master_thermostat.side_effect = Mock( - return_value=False - ) - smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.set_temperature.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.set_relay_state.side_effect = AsyncMock( - return_value=True - ) - - smile_mock.return_value.get_all_devices.return_value = _read_json( - chosen_env, "get_all_devices" - ) - smile_mock.return_value.get_device_data.side_effect = partial( - _get_device_data, chosen_env - ) - - yield smile_mock.return_value - - -@pytest.fixture(name="mock_smile_anna") -def mock_smile_anna(): +@pytest.fixture +def mock_smile_anna() -> Generator[None, MagicMock, None]: """Create a Mock Anna environment for testing exceptions.""" chosen_env = "anna_heatpump" - with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.XMLDataMissingError = XMLDataMissingError - - smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b" - smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" - smile_mock.return_value.smile_version = "4.0.15" - smile_mock.return_value.smile_type = "thermostat" - smile_mock.return_value.smile_hostname = "smile98765" - - smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") - - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.full_update_device.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.single_master_thermostat.side_effect = Mock( - return_value=True - ) - smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.set_temperature.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.set_relay_state.side_effect = AsyncMock( - return_value=True - ) - - smile_mock.return_value.get_all_devices.return_value = _read_json( - chosen_env, "get_all_devices" - ) - smile_mock.return_value.get_device_data.side_effect = partial( - _get_device_data, chosen_env - ) - - yield smile_mock.return_value - - -@pytest.fixture(name="mock_smile_p1") -def mock_smile_p1(): - """Create a Mock P1 DSMR environment for testing exceptions.""" - chosen_env = "p1v3_full_option" - with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.XMLDataMissingError = XMLDataMissingError + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value - smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" - smile_mock.return_value.heater_id = None - smile_mock.return_value.smile_version = "3.3.9" - smile_mock.return_value.smile_type = "power" - smile_mock.return_value.smile_hostname = "smile98765" + smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" + smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" + smile.smile_version = "4.0.15" + smile.smile_type = "thermostat" + smile.smile_hostname = "smile98765" + smile.smile_name = "Anna" - smile_mock.return_value.notifications = _read_json(chosen_env, "notifications") + smile.connect.return_value = True - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.full_update_device.side_effect = AsyncMock( - return_value=True - ) + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") - smile_mock.return_value.single_master_thermostat.side_effect = Mock( - return_value=None - ) + yield smile - smile_mock.return_value.get_all_devices.return_value = _read_json( - chosen_env, "get_all_devices" - ) - smile_mock.return_value.get_device_data.side_effect = partial( - _get_device_data, chosen_env - ) - yield smile_mock.return_value +@pytest.fixture +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 + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" + smile.heater_id = None + smile.smile_version = "3.3.9" + smile.smile_type = "power" + smile.smile_hostname = "smile98765" + smile.smile_name = "Smile P1" + smile.connect.return_value = True -@pytest.fixture(name="mock_stretch") -def mock_stretch(): + smile.notifications = _read_json(chosen_env, "notifications") + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + +@pytest.fixture +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") as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.XMLDataMissingError = XMLDataMissingError - - smile_mock.return_value.gateway_id = "259882df3c05415b99c2d962534ce820" - smile_mock.return_value.heater_id = None - smile_mock.return_value.smile_version = "3.1.11" - smile_mock.return_value.smile_type = "stretch" - smile_mock.return_value.smile_hostname = "stretch98765" - - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.full_update_device.side_effect = AsyncMock( - return_value=True - ) - smile_mock.return_value.set_relay_state.side_effect = AsyncMock( - return_value=True - ) - - smile_mock.return_value.get_all_devices.return_value = _read_json( - chosen_env, "get_all_devices" - ) - smile_mock.return_value.get_device_data.side_effect = partial( - _get_device_data, chosen_env - ) - - yield smile_mock.return_value + with patch( + "homeassistant.components.plugwise.gateway.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "259882df3c05415b99c2d962534ce820" + smile.heater_id = None + smile.smile_version = "3.1.11" + smile.smile_type = "stretch" + smile.smile_hostname = "stretch98765" + smile.smile_name = "Stretch" + + smile.connect.return_value = True + smile.async_update.return_value = _read_json(chosen_env, "all_data") + + yield smile + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Plugwise integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json new file mode 100644 index 0000000000000..65b96074cc086 --- /dev/null +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -0,0 +1,575 @@ +[ + { + "smile_name": "Adam", + "gateway_id": "fe799307f1624099878210aa0b9f1475", + "heater_id": "90986d591dcd426cae3ec3e8111ff730", + "cooling_present": false, + "notifications": { + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } + } + }, + { + "df4a4a8169904cdb9c03d61a21f42140": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "12493538af164a409c6a1c79e38afe1c", + "mac_address": null, + "model": "Lisa", + "name": "Zone Lisa Bios", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": [ + "home", + "asleep", + "away", + "vacation", + "no_frost" + ], + "active_preset": "away", + "presets": { + "home": [ + 20.0, + 22.0 + ], + "asleep": [ + 17.0, + 24.0 + ], + "away": [ + 15.0, + 25.0 + ], + "vacation": [ + 15.0, + 28.0 + ], + "no_frost": [ + 10.0, + 30.0 + ] + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "selected_schedule": "None", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "heat", + "sensors": { + "temperature": 16.5, + "setpoint": 13, + "battery": 67 + } + }, + "b310b72a0e354bfab43089919b9a88bf": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": null, + "model": "Tom/Floor", + "name": "Floor kraan", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 26.0, + "setpoint": 21.5, + "temperature_difference": 3.5, + "valve_position": 100 + } + }, + "a2c3583e0a6349358998b760cea82d2a": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "12493538af164a409c6a1c79e38afe1c", + "mac_address": null, + "model": "Tom/Floor", + "name": "Bios Cv Thermostatic Radiator ", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 17.2, + "setpoint": 13, + "battery": 62, + "temperature_difference": -0.2, + "valve_position": 0.0 + } + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "class": "zone_thermostat", + "fw": "2016-08-02T02:00:00+02:00", + "hw": "255", + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": null, + "model": "Lisa", + "name": "Zone Lisa WK", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": [ + "home", + "asleep", + "away", + "vacation", + "no_frost" + ], + "active_preset": "home", + "presets": { + "home": [ + 20.0, + 22.0 + ], + "asleep": [ + 17.0, + 24.0 + ], + "away": [ + 15.0, + 25.0 + ], + "vacation": [ + 15.0, + 28.0 + ], + "no_frost": [ + 10.0, + 30.0 + ] + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "selected_schedule": "GF7 Woonkamer", + "last_used": "GF7 Woonkamer", + "schedule_temperature": 20.0, + "mode": "auto", + "sensors": { + "temperature": 20.9, + "setpoint": 21.5, + "battery": 34 + } + }, + "fe799307f1624099878210aa0b9f1475": { + "class": "gateway", + "fw": "3.0.15", + "hw": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Adam", + "name": "Adam", + "vendor": "Plugwise B.V.", + "zigbee_mac_address": "ABCD012345670101", + "binary_sensors": { + "plugwise_notification": true + }, + "sensors": { + "outdoor_temperature": 7.81 + } + }, + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mac_address": null, + "model": "Tom/Floor", + "name": "Thermostatic Radiator Jessie", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 17.1, + "setpoint": 15, + "battery": 62, + "temperature_difference": 0.1, + "valve_position": 0.0 + } + }, + "21f2b542c49845e6bb416884c55778d6": { + "class": "game_console", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "Playstation Smart Plug", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12", + "sensors": { + "electricity_consumed": 82.6, + "electricity_consumed_interval": 8.6, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "78d1126fc4c743db81b61c20e88342a7": { + "class": "central_heating_pump", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": null, + "model": "Plug", + "name": "CV Pomp", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05", + "sensors": { + "electricity_consumed": 35.6, + "electricity_consumed_interval": 7.37, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true + } + }, + "90986d591dcd426cae3ec3e8111ff730": { + "class": "heater_central", + "fw": null, + "hw": null, + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": null, + "model": "Unknown", + "name": "OnOff", + "vendor": null, + "lower_bound": 10, + "upper_bound": 90, + "resolution": 1, + "binary_sensors": { + "heating_state": true + }, + "sensors": { + "water_temperature": 70.0, + "intended_boiler_temperature": 70.0, + "modulation_level": 1 + } + }, + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "class": "vcr", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "NAS", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14", + "sensors": { + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": true + } + }, + "4a810418d5394b3f82727340b91ba740": { + "class": "router", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "USG Smart Plug", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": true + } + }, + "02cf28bfec924855854c544690a609ef": { + "class": "vcr", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "NVR", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A15", + "sensors": { + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": true + } + }, + "a28f588dc4a049a483fd03a30361ad3a": { + "class": "settop", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "Fibaro HC2", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A13", + "sensors": { + "electricity_consumed": 12.5, + "electricity_consumed_interval": 3.8, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": true + } + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mac_address": null, + "model": "Lisa", + "name": "Zone Thermostat Jessie", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": [ + "home", + "asleep", + "away", + "vacation", + "no_frost" + ], + "active_preset": "asleep", + "presets": { + "home": [ + 20.0, + 22.0 + ], + "asleep": [ + 17.0, + 24.0 + ], + "away": [ + 15.0, + 25.0 + ], + "vacation": [ + 15.0, + 28.0 + ], + "no_frost": [ + 10.0, + 30.0 + ] + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "selected_schedule": "CV Jessie", + "last_used": "CV Jessie", + "schedule_temperature": 15.0, + "mode": "auto", + "sensors": { + "temperature": 17.2, + "setpoint": 15, + "battery": 37 + } + }, + "680423ff840043738f42cc7f1ff97a36": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mac_address": null, + "model": "Tom/Floor", + "name": "Thermostatic Radiator Badkamer", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 19.1, + "setpoint": 14, + "battery": 51, + "temperature_difference": -0.4, + "valve_position": 0.0 + } + }, + "f1fee6043d3642a9b0a65297455f008e": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mac_address": null, + "model": "Lisa", + "name": "Zone Thermostat Badkamer", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": [ + "home", + "asleep", + "away", + "vacation", + "no_frost" + ], + "active_preset": "away", + "presets": { + "home": [ + 20.0, + 22.0 + ], + "asleep": [ + 17.0, + 24.0 + ], + "away": [ + 15.0, + 25.0 + ], + "vacation": [ + 15.0, + 28.0 + ], + "no_frost": [ + 10.0, + 30.0 + ] + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "selected_schedule": "Badkamer Schema", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "auto", + "sensors": { + "temperature": 18.9, + "setpoint": 14, + "battery": 92 + } + }, + "675416a629f343c495449970e2ca37b5": { + "class": "router", + "fw": "2019-06-21T02:00:00+02:00", + "hw": null, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": null, + "model": "Plug", + "name": "Ziggo Modem", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true, + "lock": true + } + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "class": "thermostatic_radiator_valve", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "446ac08dd04d4eff8ac57489757b7314", + "mac_address": null, + "model": "Tom/Floor", + "name": "CV Kraan Garage", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "preset_modes": [ + "home", + "asleep", + "away", + "vacation", + "no_frost" + ], + "active_preset": "no_frost", + "presets": { + "home": [ + 20.0, + 22.0 + ], + "asleep": [ + 17.0, + 24.0 + ], + "away": [ + 15.0, + 25.0 + ], + "vacation": [ + 15.0, + 28.0 + ], + "no_frost": [ + 10.0, + 30.0 + ] + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie" + ], + "selected_schedule": "None", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "heat", + "sensors": { + "temperature": 15.6, + "setpoint": 5.5, + "battery": 68, + "temperature_difference": 0.0, + "valve_position": 0.0 + } + } + } +] \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_all_devices.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_all_devices.json deleted file mode 100644 index 5a3492a3c6b9f..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_all_devices.json +++ /dev/null @@ -1 +0,0 @@ -{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "model": "Smile Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json deleted file mode 100644 index 238da9d846a36..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json deleted file mode 100644 index 4fcb40c4cf8d7..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json deleted file mode 100644 index feb6290c9c4c9..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json deleted file mode 100644 index 74d15fac3740b..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json deleted file mode 100644 index 3ea0a92387b6e..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 19.1, "setpoint": 14.0, "battery": 51, "temperature_difference": -0.4, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json deleted file mode 100644 index 2d8ace6fa3fa8..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 17.2, "setpoint": 15.0, "battery": 37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json deleted file mode 100644 index 7a9c3e9be01ad..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json deleted file mode 100644 index d2f2f82bdf6d0..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json +++ /dev/null @@ -1 +0,0 @@ -{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 1, "heating_state": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json deleted file mode 100644 index 0aeca4cc18e59..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json deleted file mode 100644 index 3f01f47fc5c81..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 17.2, "setpoint": 13.0, "battery": 62, "temperature_difference": -0.2, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json deleted file mode 100644 index 3a1c902932ad1..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 100} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json deleted file mode 100644 index 2b314f589b60e..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 20.9, "setpoint": 21.5, "battery": 34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json deleted file mode 100644 index fbefc5bba2570..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json deleted file mode 100644 index 3e0615939535b..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 17.1, "setpoint": 15.0, "battery": 62, "temperature_difference": 0.1, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json deleted file mode 100644 index 88420a8a6bd4c..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 16.5, "setpoint": 13.0, "battery": 67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json deleted file mode 100644 index 7e4532987b033..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 15.6, "setpoint": 5.5, "battery": 68, "temperature_difference": 0.0, "valve_position": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json deleted file mode 100644 index 0d6e19967dc74..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 18.9, "setpoint": 14.0, "battery": 92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json deleted file mode 100644 index ef325af7bc2cb..0000000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json +++ /dev/null @@ -1 +0,0 @@ -{"outdoor_temperature": 7.81} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json index c229f64da0464..8749be4c34541 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json @@ -1 +1,5 @@ -{"af82e4ccf9c548528166d38e560662a4": {"warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device."}} \ No newline at end of file +{ + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } +} diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json new file mode 100644 index 0000000000000..e9e62b77bb02b --- /dev/null +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -0,0 +1,117 @@ +[ + { + "smile_name": "Anna", + "gateway_id": "015ae9ea3f964e668e490fa39da3870b", + "heater_id": "1cbf783bb11e4a7c8a6843dee3a86927", + "cooling_present": true, + "notifications": {} + }, + { + "1cbf783bb11e4a7c8a6843dee3a86927": { + "class": "heater_central", + "fw": null, + "hw": null, + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": null, + "model": "Generic heater", + "name": "OpenTherm", + "vendor": "Techneco", + "lower_bound": -10, + "upper_bound": 40, + "resolution": 1, + "compressor_state": true, + "binary_sensors": { + "dhw_state": false, + "heating_state": true, + "cooling_state": false, + "slave_boiler_state": false, + "flame_state": false + }, + "sensors": { + "outdoor_temperature": 3.0, + "water_temperature": 29.1, + "intended_boiler_temperature": 0.0, + "modulation_level": 52, + "return_temperature": 25.1, + "water_pressure": 1.57 + }, + "switches": { + "dhw_cm_switch": false + }, + "cooling_active": false + }, + "015ae9ea3f964e668e490fa39da3870b": { + "class": "gateway", + "fw": "4.0.15", + "hw": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise B.V.", + "binary_sensors": { + "plugwise_notification": false + }, + "sensors": { + "outdoor_temperature": 20.2 + } + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "class": "thermostat", + "fw": "2018-02-08T11:15:53+01:00", + "hw": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "mac_address": null, + "model": "Anna", + "name": "Anna", + "vendor": "Plugwise", + "lower_bound": 4, + "upper_bound": 30, + "resolution": 0.1, + "preset_modes": [ + "no_frost", + "home", + "away", + "asleep", + "vacation" + ], + "active_preset": "home", + "presets": { + "no_frost": [ + 10.0, + 30.0 + ], + "home": [ + 21.0, + 22.0 + ], + "away": [ + 20.0, + 25.0 + ], + "asleep": [ + 20.5, + 24.0 + ], + "vacation": [ + 17.0, + 28.0 + ] + }, + "available_schedules": [ + "None" + ], + "selected_schedule": "None", + "last_used": null, + "schedule_temperature": null, + "mode": "heat", + "sensors": { + "temperature": 19.3, + "setpoint": 21, + "illuminance": 86.0, + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4 + } + } + } +] \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/anna_heatpump/get_all_devices.json b/tests/components/plugwise/fixtures/anna_heatpump/get_all_devices.json deleted file mode 100644 index ea46cd680542b..0000000000000 --- a/tests/components/plugwise/fixtures/anna_heatpump/get_all_devices.json +++ /dev/null @@ -1 +0,0 @@ -{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "model": "Smile Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "model": "Thermostat", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json deleted file mode 100644 index 750aa8b455c07..0000000000000 --- a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json +++ /dev/null @@ -1 +0,0 @@ -{"outdoor_temperature": 20.2} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json deleted file mode 100644 index 604b93889699c..0000000000000 --- a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json +++ /dev/null @@ -1 +0,0 @@ -{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 52, "return_temperature": 25.1, "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false, "water_pressure": 1.57, "outdoor_temperature": 18.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json deleted file mode 100644 index 048cc0f77dcfb..0000000000000 --- a/tests/components/plugwise/fixtures/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json +++ /dev/null @@ -1 +0,0 @@ -{"temperature": 23.3, "setpoint": 21.0, "heating_state": false, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json new file mode 100644 index 0000000000000..fc565ef604007 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json @@ -0,0 +1,37 @@ +[ + { + "smile_name": "P1", + "gateway_id": "e950c7d5e1ee407a858e2a8b5016c8b3", + "notifications": {} + }, + { + "e950c7d5e1ee407a858e2a8b5016c8b3": { + "class": "gateway", + "fw": "3.3.9", + "hw": "AME Smile 2.0 board", + "location": "cd3e822288064775a7c4afcdd70bdda2", + "mac_address": "012345670001", + "model": "P1", + "name": "P1", + "vendor": "Plugwise B.V.", + "sensors": { + "net_electricity_point": -2816, + "electricity_consumed_peak_point": 0, + "electricity_consumed_off_peak_point": 0, + "net_electricity_cumulative": 442.972, + "electricity_consumed_peak_cumulative": 442.932, + "electricity_consumed_off_peak_cumulative": 551.09, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_off_peak_interval": 0, + "electricity_produced_peak_point": 2816, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 396.559, + "electricity_produced_off_peak_cumulative": 154.491, + "electricity_produced_peak_interval": 0, + "electricity_produced_off_peak_interval": 0, + "gas_consumed_cumulative": 584.85, + "gas_consumed_interval": 0.0 + } + } + } +] \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/get_all_devices.json b/tests/components/plugwise/fixtures/p1v3_full_option/get_all_devices.json deleted file mode 100644 index a78f45ead8a35..0000000000000 --- a/tests/components/plugwise/fixtures/p1v3_full_option/get_all_devices.json +++ /dev/null @@ -1 +0,0 @@ -{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "model": "Smile P1", "types": {"py/set": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/components/plugwise/fixtures/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json deleted file mode 100644 index eed9382a7e9ba..0000000000000 --- a/tests/components/plugwise/fixtures/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json +++ /dev/null @@ -1 +0,0 @@ -{"net_electricity_point": -2761, "electricity_consumed_peak_point": 0, "electricity_consumed_off_peak_point": 0, "net_electricity_cumulative": 442.972, "electricity_consumed_peak_cumulative": 442.932, "electricity_consumed_off_peak_cumulative": 551.09, "net_electricity_interval": 0, "electricity_consumed_peak_interval": 0, "electricity_consumed_off_peak_interval": 0, "electricity_produced_peak_point": 2761, "electricity_produced_off_peak_point": 0, "electricity_produced_peak_cumulative": 396.559, "electricity_produced_off_peak_cumulative": 154.491, "electricity_produced_peak_interval": 0, "electricity_produced_off_peak_interval": 0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json new file mode 100644 index 0000000000000..21d7a888dd59f --- /dev/null +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -0,0 +1,175 @@ +[ + { + "smile_name": "Stretch", + "gateway_id": "0000aaaa0000aaaa0000aaaa0000aa00", + "notifications": {} + }, + { + "0000aaaa0000aaaa0000aaaa0000aa00": { + "class": "gateway", + "fw": "3.1.11", + "hw": null, + "mac_address": "01:23:45:67:89:AB", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "vendor": "Plugwise B.V.", + "model": "Stretch", + "name": "Stretch", + "zigbee_mac_address": "ABCD012345670101" + }, + "5871317346d045bc9f6b987ef25ee638": { + "class": "water_heater_vessel", + "fw": "2011-06-27T10:52:18+02:00", + "hw": "6539-0701-4028", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": null, + "model": "Circle type F", + "name": "Boiler (1EB31)", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07", + "sensors": { + "electricity_consumed": 1.19, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "e1c884e7dede431dadee09506ec4f859": { + "class": "refrigerator", + "fw": "2011-06-27T10:47:37+02:00", + "hw": "6539-0700-7330", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": null, + "model": "Circle+ type F", + "name": "Koelkast (92C4A)", + "vendor": "Plugwise", + "sensors": { + "electricity_consumed": 50.5, + "electricity_consumed_interval": 0.08, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "aac7b735042c4832ac9ff33aae4f453b": { + "class": "dishwasher", + "fw": "2011-06-27T10:52:18+02:00", + "hw": "6539-0701-4022", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": null, + "model": "Circle type F", + "name": "Vaatwasser (2a1ab)", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.71, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "cfe95cf3de1948c0b8955125bf754614": { + "class": "dryer", + "fw": "2011-06-27T10:52:18+02:00", + "hw": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": null, + "model": "Circle type F", + "name": "Droger (52559)", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "059e4d03c7a34d278add5c7a4a781d19": { + "class": "washingmachine", + "fw": "2011-06-27T10:52:18+02:00", + "hw": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": null, + "model": "Circle type F", + "name": "Wasmachine (52AC1)", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true, + "lock": false + } + }, + "71e1944f2a944b26ad73323e399efef0": { + "class": "switching", + "fw": null, + "location": null, + "model": "Switchgroup", + "name": "Test", + "members": [ + "5ca521ac179d468e91d772eeeb8a2117" + ], + "types": [ + "switch_group" + ], + "vendor": null, + "switches": { + "relay": true + } + }, + "d950b314e9d8499f968e6db8d82ef78c": { + "class": "report", + "fw": null, + "location": null, + "model": "Switchgroup", + "name": "Stroomvreters", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "5871317346d045bc9f6b987ef25ee638", + "aac7b735042c4832ac9ff33aae4f453b", + "cfe95cf3de1948c0b8955125bf754614", + "e1c884e7dede431dadee09506ec4f859" + ], + "types": [ + "switch_group" + ], + "vendor": null, + "switches": { + "relay": true + } + }, + "d03738edfcc947f7b8f4573571d90d2d": { + "class": "switching", + "fw": null, + "location": null, + "model": "Switchgroup", + "name": "Schakel", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "cfe95cf3de1948c0b8955125bf754614" + ], + "types": [ + "switch_group" + ], + "vendor": null, + "switches": { + "relay": true + } + } + } +] \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_all_devices.json b/tests/components/plugwise/fixtures/stretch_v31/get_all_devices.json deleted file mode 100644 index dab74fb74a231..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_all_devices.json +++ /dev/null @@ -1 +0,0 @@ -{"5ca521ac179d468e91d772eeeb8a2117": {"name": "Oven (793F84)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "zz_misc", "location": 0}, "5871317346d045bc9f6b987ef25ee638": {"name": "Boiler (1EB31)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "water_heater_vessel", "location": 0}, "e1c884e7dede431dadee09506ec4f859": {"name": "Koelkast (92C4A)", "model": "Circle+", "types": {"py/set": ["plug", "power"]}, "class": "refrigerator", "location": 0}, "aac7b735042c4832ac9ff33aae4f453b": {"name": "Vaatwasser (2a1ab)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dishwasher", "location": 0}, "cfe95cf3de1948c0b8955125bf754614": {"name": "Droger (52559)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dryer", "location": 0}, "99f89d097be34fca88d8598c6dbc18ea": {"name": "Meterkast (787BFB)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": 0}, "059e4d03c7a34d278add5c7a4a781d19": {"name": "Wasmachine (52AC1)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "washingmachine", "location": 0}, "e309b52ea5684cf1a22f30cf0cd15051": {"name": "Computer (788618)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "computer_desktop", "location": 0}, "71e1944f2a944b26ad73323e399efef0": {"name": "Test", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["5ca521ac179d468e91d772eeeb8a2117"], "location": null}, "d950b314e9d8499f968e6db8d82ef78c": {"name": "Stroomvreters", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "report", "members": ["059e4d03c7a34d278add5c7a4a781d19", "5871317346d045bc9f6b987ef25ee638", "aac7b735042c4832ac9ff33aae4f453b", "cfe95cf3de1948c0b8955125bf754614", "e1c884e7dede431dadee09506ec4f859"], "location": null}, "d03738edfcc947f7b8f4573571d90d2d": {"name": "Schakel", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["059e4d03c7a34d278add5c7a4a781d19", "cfe95cf3de1948c0b8955125bf754614"], "location": null}} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json deleted file mode 100644 index b08f6d6093adf..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/059e4d03c7a34d278add5c7a4a781d19.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json deleted file mode 100644 index 4a3e493b246dd..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 1.19, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json deleted file mode 100644 index 7325dff827185..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json deleted file mode 100644 index bbb8ac98c1c4f..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json +++ /dev/null @@ -1 +0,0 @@ -{"relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json deleted file mode 100644 index b0cab0e3f304e..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 27.6, "electricity_consumed_interval": 28.2, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json deleted file mode 100644 index e58bc4c6d6f24..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.71, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json deleted file mode 100644 index b08f6d6093adf..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json deleted file mode 100644 index bbb8ac98c1c4f..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json +++ /dev/null @@ -1 +0,0 @@ -{"relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json deleted file mode 100644 index bbb8ac98c1c4f..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json +++ /dev/null @@ -1 +0,0 @@ -{"relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json deleted file mode 100644 index 11ebae52f49a1..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 50.5, "electricity_consumed_interval": 0.08, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json b/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json deleted file mode 100644 index 456fb6744d2ab..0000000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json +++ /dev/null @@ -1 +0,0 @@ -{"electricity_consumed": 156, "electricity_consumed_interval": 163, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 5d802fb42a012..aacb9e469bb47 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -1,49 +1,63 @@ """Tests for the Plugwise binary_sensor integration.""" -from homeassistant.config_entries import ConfigEntryState +from unittest.mock import MagicMock + from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant -from tests.components.plugwise.common import async_init_integration +from tests.common import MockConfigEntry -async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): +async def test_anna_climate_binary_sensor_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of climate related binary_sensor entities.""" - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("binary_sensor.auxiliary_slave_boiler_state") - assert str(state.state) == STATE_OFF + state = hass.states.get("binary_sensor.opentherm_secondary_boiler_state") + assert state + assert state.state == STATE_OFF - state = hass.states.get("binary_sensor.auxiliary_dhw_state") - assert str(state.state) == STATE_OFF + state = hass.states.get("binary_sensor.opentherm_dhw_state") + assert state + assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.opentherm_heating") + assert state + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.opentherm_cooling") + assert state + assert state.state == STATE_OFF -async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): - """Test change of climate related binary_sensor entities.""" - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED - hass.states.async_set("binary_sensor.auxiliary_dhw_state", STATE_ON, {}) +async def test_anna_climate_binary_sensor_change( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test change of climate related binary_sensor entities.""" + hass.states.async_set("binary_sensor.opentherm_dhw_state", STATE_ON, {}) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.auxiliary_dhw_state") - assert str(state.state) == STATE_ON + state = hass.states.get("binary_sensor.opentherm_dhw_state") + assert state + assert state.state == STATE_ON await hass.helpers.entity_component.async_update_entity( - "binary_sensor.auxiliary_dhw_state" + "binary_sensor.opentherm_dhw_state" ) - state = hass.states.get("binary_sensor.auxiliary_dhw_state") - assert str(state.state) == STATE_OFF + state = hass.states.get("binary_sensor.opentherm_dhw_state") + assert state + assert state.state == STATE_OFF -async def test_adam_climate_binary_sensor_change(hass, mock_smile_adam): +async def test_adam_climate_binary_sensor_change( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test change of climate related binary_sensor entities.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("binary_sensor.adam_plugwise_notification") - assert str(state.state) == STATE_ON - assert "unreachable" in state.attributes.get("warning_msg")[0] + assert state + assert state.state == STATE_ON + assert "warning_msg" in state.attributes + assert "unreachable" in state.attributes["warning_msg"][0] assert not state.attributes.get("error_msg") assert not state.attributes.get("other_msg") diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 2fed3d18fd218..a52e4a955a68b 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -1,102 +1,119 @@ """Tests for the Plugwise Climate integration.""" +from unittest.mock import MagicMock + from plugwise.exceptions import PlugwiseException +import pytest -from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError -from tests.components.plugwise.common import async_init_integration +from tests.common import MockConfigEntry -async def test_adam_climate_entity_attributes(hass, mock_smile_adam): +async def test_adam_climate_entity_attributes( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of adam climate device environment.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("climate.zone_lisa_wk") - attrs = state.attributes - - assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO] - - assert "preset_modes" in attrs - assert "no_frost" in attrs["preset_modes"] - assert "home" in attrs["preset_modes"] - assert attrs["current_temperature"] == 20.9 - assert attrs["temperature"] == 21.5 - - assert attrs["preset_mode"] == "home" - - assert attrs["supported_features"] == 17 + assert state + assert state.attributes["hvac_modes"] == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ] + + assert "preset_modes" in state.attributes + assert "no_frost" in state.attributes["preset_modes"] + assert "home" in state.attributes["preset_modes"] + + assert state.attributes["current_temperature"] == 20.9 + assert state.attributes["preset_mode"] == "home" + assert state.attributes["supported_features"] == 17 + assert state.attributes["temperature"] == 21.5 + assert state.attributes["min_temp"] == 0.0 + assert state.attributes["max_temp"] == 99.9 + assert state.attributes["target_temp_step"] == 0.1 state = hass.states.get("climate.zone_thermostat_jessie") - attrs = state.attributes + assert state - assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO] + assert state.attributes["hvac_modes"] == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ] - assert "preset_modes" in attrs - assert "no_frost" in attrs["preset_modes"] - assert "home" in attrs["preset_modes"] + assert "preset_modes" in state.attributes + assert "no_frost" in state.attributes["preset_modes"] + assert "home" in state.attributes["preset_modes"] - assert attrs["current_temperature"] == 17.2 - assert attrs["temperature"] == 15.0 + assert state.attributes["current_temperature"] == 17.2 + assert state.attributes["preset_mode"] == "asleep" + assert state.attributes["temperature"] == 15.0 + assert state.attributes["min_temp"] == 0.0 + assert state.attributes["max_temp"] == 99.9 + assert state.attributes["target_temp_step"] == 0.1 - assert attrs["preset_mode"] == "asleep" - -async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): +async def test_adam_climate_adjust_negative_testing( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test exceptions of climate entities.""" mock_smile_adam.set_preset.side_effect = PlugwiseException mock_smile_adam.set_schedule_state.side_effect = PlugwiseException mock_smile_adam.set_temperature.side_effect = PlugwiseException - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - - await hass.services.async_call( - "climate", - "set_temperature", - {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, - blocking=True, - ) - state = hass.states.get("climate.zone_lisa_wk") - attrs = state.attributes - assert attrs["temperature"] == 21.5 - - await hass.services.async_call( - "climate", - "set_preset_mode", - {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, - blocking=True, - ) - state = hass.states.get("climate.zone_thermostat_jessie") - attrs = state.attributes - assert attrs["preset_mode"] == "asleep" - - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.zone_thermostat_jessie", "hvac_mode": HVAC_MODE_AUTO}, - blocking=True, - ) - state = hass.states.get("climate.zone_thermostat_jessie") - attrs = state.attributes - -async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "climate", + "set_hvac_mode", + { + "entity_id": "climate.zone_thermostat_jessie", + "hvac_mode": HVAC_MODE_AUTO, + }, + blocking=True, + ) + + +async def test_adam_climate_entity_climate_changes( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test handling of user requests in adam climate device environment.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( "climate", "set_temperature", {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, blocking=True, ) - state = hass.states.get("climate.zone_lisa_wk") - attrs = state.attributes - assert attrs["temperature"] == 25.0 + assert mock_smile_adam.set_temperature.call_count == 1 + mock_smile_adam.set_temperature.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", 25.0 + ) await hass.services.async_call( "climate", @@ -104,12 +121,11 @@ async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): {"entity_id": "climate.zone_lisa_wk", "preset_mode": "away"}, blocking=True, ) - state = hass.states.get("climate.zone_lisa_wk") - attrs = state.attributes - - assert attrs["preset_mode"] == "away" - assert attrs["supported_features"] == 17 + assert mock_smile_adam.set_preset.call_count == 1 + mock_smile_adam.set_preset.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", "away" + ) await hass.services.async_call( "climate", @@ -118,10 +134,10 @@ async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): blocking=True, ) - state = hass.states.get("climate.zone_thermostat_jessie") - attrs = state.attributes - - assert attrs["temperature"] == 25.0 + assert mock_smile_adam.set_temperature.call_count == 2 + mock_smile_adam.set_temperature.assert_called_with( + "82fa13f017d240daa0d0ea1775420f24", 25.0 + ) await hass.services.async_call( "climate", @@ -129,42 +145,42 @@ async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, blocking=True, ) - state = hass.states.get("climate.zone_thermostat_jessie") - attrs = state.attributes - assert attrs["preset_mode"] == "home" + assert mock_smile_adam.set_preset.call_count == 2 + mock_smile_adam.set_preset.assert_called_with( + "82fa13f017d240daa0d0ea1775420f24", "home" + ) -async def test_anna_climate_entity_attributes(hass, mock_smile_anna): +async def test_anna_climate_entity_attributes( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of anna climate device environment.""" - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("climate.anna") - attrs = state.attributes - - assert "hvac_modes" in attrs - assert "heat_cool" in attrs["hvac_modes"] - - assert "preset_modes" in attrs - assert "no_frost" in attrs["preset_modes"] - assert "home" in attrs["preset_modes"] - - assert attrs["current_temperature"] == 23.3 - assert attrs["temperature"] == 21.0 - - assert state.state == HVAC_MODE_AUTO - assert attrs["hvac_action"] == "idle" - assert attrs["preset_mode"] == "home" - - assert attrs["supported_features"] == 17 - - -async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes["hvac_modes"] == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_COOL, + ] + assert "no_frost" in state.attributes["preset_modes"] + assert "home" in state.attributes["preset_modes"] + + assert state.attributes["current_temperature"] == 19.3 + assert state.attributes["hvac_action"] == "heating" + assert state.attributes["preset_mode"] == "home" + assert state.attributes["supported_features"] == 17 + assert state.attributes["temperature"] == 21.0 + assert state.attributes["min_temp"] == 4.0 + assert state.attributes["max_temp"] == 30.0 + assert state.attributes["target_temp_step"] == 0.1 + + +async def test_anna_climate_entity_climate_changes( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: """Test handling of user requests in anna climate device environment.""" - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( "climate", "set_temperature", @@ -172,10 +188,10 @@ async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): blocking=True, ) - state = hass.states.get("climate.anna") - attrs = state.attributes - - assert attrs["temperature"] == 25.0 + assert mock_smile_anna.set_temperature.call_count == 1 + mock_smile_anna.set_temperature.assert_called_with( + "c784ee9fdab44e1395b8dee7d7a497d5", 25.0 + ) await hass.services.async_call( "climate", @@ -184,10 +200,10 @@ async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): blocking=True, ) - state = hass.states.get("climate.anna") - attrs = state.attributes - - assert attrs["preset_mode"] == "away" + assert mock_smile_anna.set_preset.call_count == 1 + mock_smile_anna.set_preset.assert_called_with( + "c784ee9fdab44e1395b8dee7d7a497d5", "away" + ) await hass.services.async_call( "climate", @@ -196,7 +212,20 @@ async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): blocking=True, ) - state = hass.states.get("climate.anna") - attrs = state.attributes + assert mock_smile_anna.set_temperature.call_count == 1 + assert mock_smile_anna.set_schedule_state.call_count == 1 + mock_smile_anna.set_schedule_state.assert_called_with( + "c784ee9fdab44e1395b8dee7d7a497d5", None, "off" + ) - assert state.state == "heat_cool" + # Auto mode is not available, no schedules + with pytest.raises(ValueError): + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.anna", "hvac_mode": "auto"}, + blocking=True, + ) + + assert mock_smile_anna.set_temperature.call_count == 1 + assert mock_smile_anna.set_schedule_state.call_count == 1 diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 9f9be299f843b..9fdd032351823 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -8,27 +8,23 @@ ) import pytest -from homeassistant.components import zeroconf -from homeassistant.components.plugwise.const import ( - API, - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - FLOW_NET, - FLOW_TYPE, - PW_TYPE, -) +from homeassistant.components.plugwise.const import API, DEFAULT_PORT, DOMAIN, PW_TYPE +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SOURCE, CONF_USERNAME, ) -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.common import MockConfigEntry @@ -40,8 +36,9 @@ TEST_USERNAME = "smile" TEST_USERNAME2 = "stretch" -TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( +TEST_DISCOVERY = ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname=f"{TEST_HOSTNAME}.local.", name="mock_name", port=DEFAULT_PORT, @@ -52,8 +49,10 @@ }, type="mock_type", ) -TEST_DISCOVERY2 = zeroconf.ZeroconfServiceInfo( + +TEST_DISCOVERY2 = ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname=f"{TEST_HOSTNAME2}.local.", name="mock_name", port=DEFAULT_PORT, @@ -79,49 +78,32 @@ def mock_smile(): yield smile_mock.return_value -async def test_form_flow_gateway(hass): - """Test we get the form for Plugwise Gateway product type.""" - +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, +) -> None: + """Test the full user configuration flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={FLOW_TYPE: FLOW_NET} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "user_gateway" - - -async def test_form(hass): - """Test we get the form.""" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + }, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - ) - await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, @@ -130,72 +112,79 @@ async def test_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zeroconf_form(hass): - """Test we get the form.""" - +@pytest.mark.parametrize( + "discovery,username", + [ + (TEST_DISCOVERY, TEST_USERNAME), + (TEST_DISCOVERY2, TEST_USERNAME2), + ], +) +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, + discovery: ZeroconfServiceInfo, + username: str, +) -> None: + """Test config flow for smile devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=TEST_DISCOVERY, + data=discovery, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: TEST_PASSWORD}, + ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, - CONF_USERNAME: TEST_USERNAME, + CONF_USERNAME: username, PW_TYPE: API, } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zeroconf_stretch_form(hass): - """Test we get the form.""" - +async def test_zeroconf_flow_stretch( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, +) -> None: + """Test config flow for stretch devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: TEST_PASSWORD}, + ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, @@ -204,9 +193,10 @@ async def test_zeroconf_stretch_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zercoconf_discovery_update_configuration(hass): +async def test_zercoconf_discovery_update_configuration(hass: HomeAssistant) -> None: """Test if a discovered device is configured and updated with new host.""" entry = MockConfigEntry( domain=DOMAIN, @@ -224,219 +214,65 @@ async def test_zercoconf_discovery_update_configuration(hass): data=TEST_DISCOVERY, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" -async def test_form_username(hass): - """Test we get the username data back.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile", - ) as smile_mock, patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.gateway_id = "abcdefgh12345678" - smile_mock.return_value.smile_hostname = TEST_HOST - smile_mock.return_value.smile_name = "Adam" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_USERNAME: TEST_USERNAME2, - }, - ) - - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: DEFAULT_PORT, - CONF_USERNAME: TEST_USERNAME2, - PW_TYPE: API, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=TEST_DISCOVERY, - ) - assert result3["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.plugwise.config_flow.Smile", - ) as smile_mock, patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - smile_mock.return_value.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.gateway_id = "abcdefgh12345678" - smile_mock.return_value.smile_hostname = TEST_HOST - smile_mock.return_value.smile_name = "Adam" - - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) - - await hass.async_block_till_done() - - assert result4["type"] == "abort" - assert result4["reason"] == "already_configured" - - -async def test_form_invalid_auth(hass, mock_smile): +@pytest.mark.parametrize( + "side_effect,reason", + [ + (InvalidAuthentication, "invalid_auth"), + (ConnectionFailedError, "cannot_connect"), + (PlugwiseException, "cannot_connect"), + (RuntimeError, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, + side_effect: Exception, + reason: str, +) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - - mock_smile.connect.side_effect = InvalidAuthentication - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_cannot_connect(hass, mock_smile): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result - mock_smile.connect.side_effect = ConnectionFailedError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - + mock_smile_config_flow.connect.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_cannot_connect_port(hass, mock_smile): - """Test we handle cannot connect to port error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": reason} + assert result2.get("step_id") == "user" - mock_smile.connect.side_effect = ConnectionFailedError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" + assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: TEST_PORT, - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_other_problem(hass, mock_smile): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - - mock_smile.connect.side_effect = TimeoutError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - - result2 = await hass.config_entries.flow.async_configure( + mock_smile_config_flow.connect.side_effect = None + result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} - - -async def test_options_flow_power(hass, mock_smile) -> None: - """Test config flow options DSMR environments.""" - entry = MockConfigEntry( - domain=DOMAIN, - title=CONF_NAME, - data={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) - - hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="power")}} - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.plugwise.async_setup_entry", return_value=True - ): - assert 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"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10} - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_SCAN_INTERVAL: 10, - } - - -async def test_options_flow_thermo(hass, mock_smile) -> None: - """Test config flow options for thermostatic environments.""" - entry = MockConfigEntry( - domain=DOMAIN, - title=CONF_NAME, - data={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) - - hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="thermostat")}} - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.plugwise.async_setup_entry", return_value=True - ): - assert 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"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 60} - ) + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Test Smile Name" + assert result3.get("data") == { + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: DEFAULT_PORT, + CONF_USERNAME: TEST_USERNAME, + PW_TYPE: API, + } - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_SCAN_INTERVAL: 60, - } + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 2 diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py new file mode 100644 index 0000000000000..67ab7728d1c27 --- /dev/null +++ b/tests/components/plugwise/test_diagnostics.py @@ -0,0 +1,448 @@ +"""Tests for the diagnostics data provided by the Plugwise integration.""" +from unittest.mock import MagicMock + +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mock_smile_adam: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "gateway": { + "smile_name": "Adam", + "gateway_id": "fe799307f1624099878210aa0b9f1475", + "heater_id": "90986d591dcd426cae3ec3e8111ff730", + "cooling_present": False, + "notifications": { + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } + }, + }, + "devices": { + "df4a4a8169904cdb9c03d61a21f42140": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "12493538af164a409c6a1c79e38afe1c", + "mac_address": None, + "model": "Lisa", + "name": "Zone Lisa Bios", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "active_preset": "away", + "presets": { + "home": [20.0, 22.0], + "asleep": [17.0, 24.0], + "away": [15.0, 25.0], + "vacation": [15.0, 28.0], + "no_frost": [10.0, 30.0], + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "selected_schedule": "None", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "heat", + "sensors": {"temperature": 16.5, "setpoint": 13, "battery": 67}, + }, + "b310b72a0e354bfab43089919b9a88bf": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": None, + "model": "Tom/Floor", + "name": "Floor kraan", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 26.0, + "setpoint": 21.5, + "temperature_difference": 3.5, + "valve_position": 100, + }, + }, + "a2c3583e0a6349358998b760cea82d2a": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "12493538af164a409c6a1c79e38afe1c", + "mac_address": None, + "model": "Tom/Floor", + "name": "Bios Cv Thermostatic Radiator ", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 17.2, + "setpoint": 13, + "battery": 62, + "temperature_difference": -0.2, + "valve_position": 0.0, + }, + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "class": "zone_thermostat", + "fw": "2016-08-02T02:00:00+02:00", + "hw": "255", + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": None, + "model": "Lisa", + "name": "Zone Lisa WK", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "active_preset": "home", + "presets": { + "home": [20.0, 22.0], + "asleep": [17.0, 24.0], + "away": [15.0, 25.0], + "vacation": [15.0, 28.0], + "no_frost": [10.0, 30.0], + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "selected_schedule": "GF7 Woonkamer", + "last_used": "GF7 Woonkamer", + "schedule_temperature": 20.0, + "mode": "auto", + "sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34}, + }, + "fe799307f1624099878210aa0b9f1475": { + "class": "gateway", + "fw": "3.0.15", + "hw": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Adam", + "name": "Adam", + "vendor": "Plugwise B.V.", + "zigbee_mac_address": "ABCD012345670101", + "binary_sensors": {"plugwise_notification": True}, + "sensors": {"outdoor_temperature": 7.81}, + }, + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mac_address": None, + "model": "Tom/Floor", + "name": "Thermostatic Radiator Jessie", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 17.1, + "setpoint": 15, + "battery": 62, + "temperature_difference": 0.1, + "valve_position": 0.0, + }, + }, + "21f2b542c49845e6bb416884c55778d6": { + "class": "game_console", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "Playstation Smart Plug", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12", + "sensors": { + "electricity_consumed": 82.6, + "electricity_consumed_interval": 8.6, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": False}, + }, + "78d1126fc4c743db81b61c20e88342a7": { + "class": "central_heating_pump", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "c50f167537524366a5af7aa3942feb1e", + "mac_address": None, + "model": "Plug", + "name": "CV Pomp", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05", + "sensors": { + "electricity_consumed": 35.6, + "electricity_consumed_interval": 7.37, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True}, + }, + "90986d591dcd426cae3ec3e8111ff730": { + "class": "heater_central", + "fw": None, + "hw": None, + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": None, + "model": "Unknown", + "name": "OnOff", + "vendor": None, + "lower_bound": 10, + "upper_bound": 90, + "resolution": 1, + "binary_sensors": {"heating_state": True}, + "sensors": { + "water_temperature": 70.0, + "intended_boiler_temperature": 70.0, + "modulation_level": 1, + }, + }, + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "class": "vcr", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "NAS", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14", + "sensors": { + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": True}, + }, + "4a810418d5394b3f82727340b91ba740": { + "class": "router", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "USG Smart Plug", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": True}, + }, + "02cf28bfec924855854c544690a609ef": { + "class": "vcr", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "NVR", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A15", + "sensors": { + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": True}, + }, + "a28f588dc4a049a483fd03a30361ad3a": { + "class": "settop", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "Fibaro HC2", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A13", + "sensors": { + "electricity_consumed": 12.5, + "electricity_consumed_interval": 3.8, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": True}, + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "82fa13f017d240daa0d0ea1775420f24", + "mac_address": None, + "model": "Lisa", + "name": "Zone Thermostat Jessie", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "active_preset": "asleep", + "presets": { + "home": [20.0, 22.0], + "asleep": [17.0, 24.0], + "away": [15.0, 25.0], + "vacation": [15.0, 28.0], + "no_frost": [10.0, 30.0], + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "selected_schedule": "CV Jessie", + "last_used": "CV Jessie", + "schedule_temperature": 15.0, + "mode": "auto", + "sensors": {"temperature": 17.2, "setpoint": 15, "battery": 37}, + }, + "680423ff840043738f42cc7f1ff97a36": { + "class": "thermo_sensor", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mac_address": None, + "model": "Tom/Floor", + "name": "Thermostatic Radiator Badkamer", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "sensors": { + "temperature": 19.1, + "setpoint": 14, + "battery": 51, + "temperature_difference": -0.4, + "valve_position": 0.0, + }, + }, + "f1fee6043d3642a9b0a65297455f008e": { + "class": "zone_thermostat", + "fw": "2016-10-27T02:00:00+02:00", + "hw": "255", + "location": "08963fec7c53423ca5680aa4cb502c63", + "mac_address": None, + "model": "Lisa", + "name": "Zone Thermostat Badkamer", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 99.9, + "resolution": 0.01, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "active_preset": "away", + "presets": { + "home": [20.0, 22.0], + "asleep": [17.0, 24.0], + "away": [15.0, 25.0], + "vacation": [15.0, 28.0], + "no_frost": [10.0, 30.0], + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "selected_schedule": "Badkamer Schema", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "auto", + "sensors": {"temperature": 18.9, "setpoint": 14, "battery": 92}, + }, + "675416a629f343c495449970e2ca37b5": { + "class": "router", + "fw": "2019-06-21T02:00:00+02:00", + "hw": None, + "location": "cd143c07248f491493cea0533bc3d669", + "mac_address": None, + "model": "Plug", + "name": "Ziggo Modem", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0, + }, + "switches": {"relay": True, "lock": True}, + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "class": "thermostatic_radiator_valve", + "fw": "2019-03-27T01:00:00+01:00", + "hw": "1", + "location": "446ac08dd04d4eff8ac57489757b7314", + "mac_address": None, + "model": "Tom/Floor", + "name": "CV Kraan Garage", + "vendor": "Plugwise", + "lower_bound": 0, + "upper_bound": 100.0, + "resolution": 0.01, + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "active_preset": "no_frost", + "presets": { + "home": [20.0, 22.0], + "asleep": [17.0, 24.0], + "away": [15.0, 25.0], + "vacation": [15.0, 28.0], + "no_frost": [10.0, 30.0], + }, + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + ], + "selected_schedule": "None", + "last_used": "Badkamer Schema", + "schedule_temperature": 15.0, + "mode": "heat", + "sensors": { + "temperature": 15.6, + "setpoint": 5.5, + "battery": 68, + "temperature_difference": 0.0, + "valve_position": 0.0, + }, + }, + }, + } diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index c4f7e1c6b3d0a..d63b81bd0d33c 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -1,65 +1,62 @@ """Tests for the Plugwise Climate integration.""" - import asyncio +from unittest.mock import MagicMock -from plugwise.exceptions import XMLDataMissingError +from plugwise.exceptions import ( + ConnectionFailedError, + PlugwiseException, + XMLDataMissingError, +) +import pytest from homeassistant.components.plugwise.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant -from tests.common import AsyncMock, MockConfigEntry -from tests.components.plugwise.common import async_init_integration - - -async def test_smile_unauthorized(hass, mock_smile_unauth): - """Test failing unauthorization by Smile.""" - entry = await async_init_integration(hass, mock_smile_unauth) - assert entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_smile_error(hass, mock_smile_error): - """Test server error handling by Smile.""" - entry = await async_init_integration(hass, mock_smile_error) - assert entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_smile_notconnect(hass, mock_smile_notconnect): - """Connection failure error handling by Smile.""" - mock_smile_notconnect.connect.return_value = False - entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state is ConfigEntryState.SETUP_RETRY +from tests.common import MockConfigEntry -async def test_smile_timeout(hass, mock_smile_notconnect): - """Timeout error handling by Smile.""" - mock_smile_notconnect.connect.side_effect = asyncio.TimeoutError - entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_smile_adam_xmlerror(hass, mock_smile_adam): - """Detect malformed XML by Smile in Adam environment.""" - mock_smile_adam.full_update_device.side_effect = XMLDataMissingError - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_unload_entry(hass, mock_smile_adam): - """Test being able to unload an entry.""" - entry = await async_init_integration(hass, mock_smile_adam) - - mock_smile_adam.async_reset = AsyncMock(return_value=True) - await hass.config_entries.async_unload(entry.entry_id) +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smile_anna: MagicMock, +) -> None: + """Test the Plugwise configuration entry loading/unloading.""" + 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 entry.state is ConfigEntryState.NOT_LOADED - assert not hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_smile_anna.connect.mock_calls) == 1 -async def test_async_setup_entry_fail(hass): - """Test async_setup_entry.""" - entry = MockConfigEntry(domain=DOMAIN, data={}) + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "side_effect", + [ + (ConnectionFailedError), + (PlugwiseException), + (XMLDataMissingError), + (asyncio.TimeoutError), + ], +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smile_anna: MagicMock, + side_effect: Exception, +) -> None: + """Test the Plugwise configuration entry not ready.""" + mock_smile_anna.connect.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 entry.state is ConfigEntryState.SETUP_ERROR + + assert len(mock_smile_anna.connect.mock_calls) == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 3b5bff781e5f2..6f5309d3810cd 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -1,26 +1,30 @@ """Tests for the Plugwise Sensor integration.""" -from homeassistant.config_entries import ConfigEntryState +from unittest.mock import MagicMock -from tests.common import Mock -from tests.components.plugwise.common import async_init_integration +from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry -async def test_adam_climate_sensor_entities(hass, mock_smile_adam): - """Test creation of climate related sensor entities.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED +async def test_adam_climate_sensor_entities( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of climate related sensor entities.""" state = hass.states.get("sensor.adam_outdoor_temperature") + assert state assert float(state.state) == 7.81 state = hass.states.get("sensor.cv_pomp_electricity_consumed") + assert state assert float(state.state) == 35.6 - state = hass.states.get("sensor.auxiliary_water_temperature") + state = hass.states.get("sensor.onoff_water_temperature") + assert state assert float(state.state) == 70.0 state = hass.states.get("sensor.cv_pomp_electricity_consumed_interval") + assert state assert float(state.state) == 7.37 await hass.helpers.entity_component.async_update_entity( @@ -28,62 +32,70 @@ async def test_adam_climate_sensor_entities(hass, mock_smile_adam): ) state = hass.states.get("sensor.zone_lisa_wk_battery") + assert state assert int(state.state) == 34 -async def test_anna_as_smt_climate_sensor_entities(hass, mock_smile_anna): +async def test_anna_as_smt_climate_sensor_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of climate related sensor entities.""" - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED - - state = hass.states.get("sensor.auxiliary_outdoor_temperature") - assert float(state.state) == 18.0 + state = hass.states.get("sensor.opentherm_outdoor_temperature") + assert state + assert float(state.state) == 3.0 - state = hass.states.get("sensor.auxiliary_water_temperature") + state = hass.states.get("sensor.opentherm_water_temperature") + assert state assert float(state.state) == 29.1 state = hass.states.get("sensor.anna_illuminance") + assert state assert float(state.state) == 86.0 -async def test_anna_climate_sensor_entities(hass, mock_smile_anna): +async def test_anna_climate_sensor_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of climate related sensor entities as single master thermostat.""" - mock_smile_anna.single_master_thermostat.side_effect = Mock(return_value=False) - entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state is ConfigEntryState.LOADED + mock_smile_anna.single_master_thermostat.return_value = False + state = hass.states.get("sensor.opentherm_outdoor_temperature") + assert state + assert float(state.state) == 3.0 - state = hass.states.get("sensor.auxiliary_outdoor_temperature") - assert float(state.state) == 18.0 - -async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): +async def test_p1_dsmr_sensor_entities( + hass: HomeAssistant, mock_smile_p1: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of power related sensor entities.""" - entry = await async_init_integration(hass, mock_smile_p1) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("sensor.p1_net_electricity_point") - assert float(state.state) == -2761.0 + assert state + assert float(state.state) == -2816.0 state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") + assert state assert float(state.state) == 551.09 state = hass.states.get("sensor.p1_electricity_produced_peak_point") - assert float(state.state) == 2761.0 + assert state + assert float(state.state) == 2816.0 state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") + assert state assert float(state.state) == 442.932 state = hass.states.get("sensor.p1_gas_consumed_cumulative") + assert state assert float(state.state) == 584.85 -async def test_stretch_sensor_entities(hass, mock_stretch): +async def test_stretch_sensor_entities( + hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of power related sensor entities.""" - entry = await async_init_integration(hass, mock_stretch) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("sensor.koelkast_92c4a_electricity_consumed") + assert state assert float(state.state) == 50.5 state = hass.states.get("sensor.droger_52559_electricity_consumed_interval") + assert state assert float(state.state) == 0.0 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 6355362fbd989..74e19e987d7c5 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -1,123 +1,192 @@ """Tests for the Plugwise switch integration.""" +from unittest.mock import MagicMock from plugwise.exceptions import PlugwiseException +import pytest -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.plugwise.const import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er -from tests.components.plugwise.common import async_init_integration +from tests.common import MockConfigEntry -async def test_adam_climate_switch_entities(hass, mock_smile_adam): +async def test_adam_climate_switch_entities( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test creation of climate related switch entities.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED + state = hass.states.get("switch.cv_pomp_relay") + assert state + assert state.state == "on" - state = hass.states.get("switch.cv_pomp") - assert str(state.state) == "on" + state = hass.states.get("switch.fibaro_hc2_relay") + assert state + assert state.state == "on" - state = hass.states.get("switch.fibaro_hc2") - assert str(state.state) == "on" - -async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): +async def test_adam_climate_switch_negative_testing( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +): """Test exceptions of climate related switch entities.""" - mock_smile_adam.set_relay_state.side_effect = PlugwiseException - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - - await hass.services.async_call( - "switch", - "turn_off", - {"entity_id": "switch.cv_pomp"}, - blocking=True, + mock_smile_adam.set_switch_state.side_effect = PlugwiseException + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.cv_pomp_relay"}, + blocking=True, + ) + + assert mock_smile_adam.set_switch_state.call_count == 1 + mock_smile_adam.set_switch_state.assert_called_with( + "78d1126fc4c743db81b61c20e88342a7", None, "relay", "off" ) - state = hass.states.get("switch.cv_pomp") - assert str(state.state) == "on" - await hass.services.async_call( - "switch", - "turn_on", - {"entity_id": "switch.fibaro_hc2"}, - blocking=True, + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.fibaro_hc2_relay"}, + blocking=True, + ) + + assert mock_smile_adam.set_switch_state.call_count == 2 + mock_smile_adam.set_switch_state.assert_called_with( + "a28f588dc4a049a483fd03a30361ad3a", None, "relay", "on" ) - state = hass.states.get("switch.fibaro_hc2") - assert str(state.state) == "on" -async def test_adam_climate_switch_changes(hass, mock_smile_adam): +async def test_adam_climate_switch_changes( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: """Test changing of climate related switch entities.""" - entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( "switch", "turn_off", - {"entity_id": "switch.cv_pomp"}, + {"entity_id": "switch.cv_pomp_relay"}, blocking=True, ) - state = hass.states.get("switch.cv_pomp") - assert str(state.state) == "off" + + assert mock_smile_adam.set_switch_state.call_count == 1 + mock_smile_adam.set_switch_state.assert_called_with( + "78d1126fc4c743db81b61c20e88342a7", None, "relay", "off" + ) await hass.services.async_call( "switch", "toggle", - {"entity_id": "switch.fibaro_hc2"}, + {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) - state = hass.states.get("switch.fibaro_hc2") - assert str(state.state) == "off" + + assert mock_smile_adam.set_switch_state.call_count == 2 + mock_smile_adam.set_switch_state.assert_called_with( + "a28f588dc4a049a483fd03a30361ad3a", None, "relay", "off" + ) await hass.services.async_call( "switch", - "toggle", - {"entity_id": "switch.fibaro_hc2"}, + "turn_on", + {"entity_id": "switch.fibaro_hc2_relay"}, blocking=True, ) - state = hass.states.get("switch.fibaro_hc2") - assert str(state.state) == "on" + assert mock_smile_adam.set_switch_state.call_count == 3 + mock_smile_adam.set_switch_state.assert_called_with( + "a28f588dc4a049a483fd03a30361ad3a", None, "relay", "on" + ) -async def test_stretch_switch_entities(hass, mock_stretch): - """Test creation of climate related switch entities.""" - entry = await async_init_integration(hass, mock_stretch) - assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("switch.koelkast_92c4a") - assert str(state.state) == "on" +async def test_stretch_switch_entities( + hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of climate related switch entities.""" + state = hass.states.get("switch.koelkast_92c4a_relay") + assert state + assert state.state == "on" - state = hass.states.get("switch.droger_52559") - assert str(state.state) == "on" + state = hass.states.get("switch.droger_52559_relay") + assert state + assert state.state == "on" -async def test_stretch_switch_changes(hass, mock_stretch): +async def test_stretch_switch_changes( + hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry +) -> None: """Test changing of power related switch entities.""" - entry = await async_init_integration(hass, mock_stretch) - assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( "switch", "turn_off", - {"entity_id": "switch.koelkast_92c4a"}, + {"entity_id": "switch.koelkast_92c4a_relay"}, blocking=True, ) - - state = hass.states.get("switch.koelkast_92c4a") - assert str(state.state) == "off" + assert mock_stretch.set_switch_state.call_count == 1 + mock_stretch.set_switch_state.assert_called_with( + "e1c884e7dede431dadee09506ec4f859", None, "relay", "off" + ) await hass.services.async_call( "switch", "toggle", - {"entity_id": "switch.droger_52559"}, + {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) - state = hass.states.get("switch.droger_52559") - assert str(state.state) == "off" + assert mock_stretch.set_switch_state.call_count == 2 + mock_stretch.set_switch_state.assert_called_with( + "cfe95cf3de1948c0b8955125bf754614", None, "relay", "off" + ) await hass.services.async_call( "switch", - "toggle", - {"entity_id": "switch.droger_52559"}, + "turn_on", + {"entity_id": "switch.droger_52559_relay"}, blocking=True, ) - state = hass.states.get("switch.droger_52559") - assert str(state.state) == "on" + assert mock_stretch.set_switch_state.call_count == 3 + mock_stretch.set_switch_state.assert_called_with( + "cfe95cf3de1948c0b8955125bf754614", None, "relay", "on" + ) + + +async def test_unique_id_migration_plug_relay( + hass: HomeAssistant, mock_smile_adam: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Test unique ID migration of -plugs to -relay.""" + mock_config_entry.add_to_hass(hass) + + registry = er.async_get(hass) + # Entry to migrate + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "21f2b542c49845e6bb416884c55778d6-plug", + config_entry=mock_config_entry, + suggested_object_id="playstation_smart_plug", + disabled_by=None, + ) + # Entry not needing migration + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "675416a629f343c495449970e2ca37b5-relay", + config_entry=mock_config_entry, + suggested_object_id="router", + disabled_by=None, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("switch.playstation_smart_plug") is not None + assert hass.states.get("switch.router") is not None + + entity_entry = registry.async_get("switch.playstation_smart_plug") + assert entity_entry + assert entity_entry.unique_id == "21f2b542c49845e6bb416884c55778d6-relay" + + entity_entry = registry.async_get("switch.router") + assert entity_entry + assert entity_entry.unique_id == "675416a629f343c495449970e2ca37b5-relay" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 9d253c3a74b0b..1eac031981937 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -16,6 +16,8 @@ from tests.common import load_fixture +MOCK_GATEWAY_DIN = "111-0----2-000000000FFA" + async def _mock_powerwall_with_fixtures(hass): """Mock data used to build powerwall state.""" @@ -70,6 +72,7 @@ async def _mock_powerwall_site_name(hass, site_name): # Sets site_info_resp.site_name to return site_name site_info_resp.response["site_name"] = site_name powerwall_mock.get_site_info = Mock(return_value=site_info_resp) + powerwall_mock.get_gateway_din = Mock(return_value=MOCK_GATEWAY_DIN) return powerwall_mock diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 29a04a7008530..3ef9e9c0fd1ff 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -12,8 +12,13 @@ from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT -from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name +from .mocks import ( + MOCK_GATEWAY_DIN, + _mock_powerwall_side_effect, + _mock_powerwall_site_name, +) from tests.common import MockConfigEntry @@ -162,34 +167,63 @@ async def test_already_configured_with_ignored(hass): ) config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="1.1.1.1", - macaddress="AA:BB:CC:DD:EE:FF", - hostname="any", - ), - ) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Some site" + assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} + assert len(mock_setup_entry.mock_calls) == 1 -async def test_dhcp_discovery(hass): - """Test we can process the discovery from dhcp.""" +async def test_dhcp_discovery_manual_configure(hass): + """Test we can process the discovery from dhcp and manually configure.""" + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="1.1.1.1", - macaddress="AA:BB:CC:DD:EE:FF", - hostname="any", - ), - ) + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall.login", + side_effect=AccessDeniedError("xyz"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="any", + ), + ) assert result["type"] == "form" assert result["errors"] == {} - mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") with patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, @@ -209,18 +243,80 @@ async def test_dhcp_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_dhcp_discovery_auto_configure(hass): + """Test we can process the discovery from dhcp and auto configure.""" + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Some site" + assert result2["data"] == {"ip_address": "1.1.1.1", "password": "00GGX"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_cannot_connect(hass): + """Test we can process the discovery from dhcp and we cannot connect.""" + mock_powerwall = _mock_powerwall_side_effect(site_info=PowerwallUnreachableError) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="00GGX", + ), + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + async def test_form_reauth(hass): """Test reauthenticate.""" entry = MockConfigEntry( domain=DOMAIN, data=VALID_CONFIG, - unique_id="1.2.3.4", + unique_id=MOCK_GATEWAY_DIN, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, ) assert result["type"] == "form" assert result["errors"] == {} @@ -237,7 +333,6 @@ async def test_form_reauth(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "new-test-password", }, ) @@ -246,3 +341,68 @@ async def test_form_reauth(hass): assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_update_ip_address(hass): + """Test we can update the ip address from dhcp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" + + +async def test_dhcp_discovery_updates_unique_id(hass): + """Test we can update the unique id from dhcp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="1.2.3.4", + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + assert entry.unique_id == MOCK_GATEWAY_DIN diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index d1b0c7c2130ce..096f1405168e6 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -2,24 +2,56 @@ from dataclasses import dataclass import datetime from http import HTTPStatus -import unittest.mock as mock +from unittest import mock import prometheus_client import pytest -from homeassistant.components import climate, counter, humidifier, lock, sensor -from homeassistant.components.demo.binary_sensor import DemoBinarySensor -from homeassistant.components.demo.light import DemoLight -from homeassistant.components.demo.number import DemoNumber -from homeassistant.components.demo.sensor import DemoSensor -import homeassistant.components.prometheus as prometheus +from homeassistant.components import ( + binary_sensor, + climate, + counter, + device_tracker, + humidifier, + input_boolean, + input_number, + light, + lock, + person, + prometheus, + sensor, + switch, +) +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HUMIDITY, + ATTR_HVAC_ACTION, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, +) +from homeassistant.components.humidifier.const import ATTR_AVAILABLE_MODES from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_MODE, + ATTR_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, + PERCENTAGE, + STATE_HOME, + STATE_LOCKED, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -39,6 +71,7 @@ class FilterTest: should_pass: bool +@pytest.fixture(name="client") async def setup_prometheus_client(hass, hass_client, namespace): """Initialize an hass_client with Prometheus component.""" # Reset registry @@ -71,28 +104,9 @@ async def generate_latest_metrics(client): return body -async def test_view_empty_namespace(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_view_empty_namespace(client, sensor_entities): """Test prometheus metrics view.""" - client = await setup_prometheus_client(hass, hass_client, "") - - sensor2 = DemoSensor( - None, - "Radio Energy", - 14, - SensorDeviceClass.POWER, - None, - ENERGY_KILO_WATT_HOUR, - None, - ) - sensor2.hass = hass - sensor2.entity_id = "sensor.radio_energy" - with mock.patch( - "homeassistant.util.dt.utcnow", - return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), - ): - await sensor2.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert "# HELP python_info Python platform information" in body @@ -114,21 +128,9 @@ async def test_view_empty_namespace(hass, hass_client): ) -async def test_view_default_namespace(hass, hass_client): +@pytest.mark.parametrize("namespace", [None]) +async def test_view_default_namespace(client, sensor_entities): """Test prometheus metrics view.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, None) - - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) - await hass.async_block_till_done() - body = await generate_latest_metrics(client) assert "# HELP python_info Python platform information" in body @@ -144,73 +146,9 @@ async def test_view_default_namespace(hass, hass_client): ) -async def test_sensor_unit(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_sensor_unit(client, sensor_entities): """Test prometheus metrics for sensors with a unit.""" - client = await setup_prometheus_client(hass, hass_client, "") - - sensor1 = DemoSensor( - None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None - ) - sensor1.hass = hass - sensor1.entity_id = "sensor.television_energy" - await sensor1.async_update_ha_state() - - sensor2 = DemoSensor( - None, - "Radio Energy", - 14, - SensorDeviceClass.POWER, - None, - ENERGY_KILO_WATT_HOUR, - None, - ) - sensor2.hass = hass - sensor2.entity_id = "sensor.radio_energy" - with mock.patch( - "homeassistant.util.dt.utcnow", - return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), - ): - await sensor2.async_update_ha_state() - - sensor3 = DemoSensor( - None, - "Electricity price", - 0.123, - None, - None, - f"SEK/{ENERGY_KILO_WATT_HOUR}", - None, - ) - sensor3.hass = hass - sensor3.entity_id = "sensor.electricity_price" - await sensor3.async_update_ha_state() - - sensor4 = DemoSensor(None, "Wind Direction", 25, None, None, DEGREE, None) - sensor4.hass = hass - sensor4.entity_id = "sensor.wind_direction" - await sensor4.async_update_ha_state() - - sensor5 = DemoSensor( - None, - "SPS30 PM <1µm Weight concentration", - 3.7069, - None, - None, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - None, - ) - sensor5.hass = hass - sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" - await sensor5.async_update_ha_state() - - sensor6 = DemoSensor( - None, "Target temperature", 22.7, None, None, TEMP_CELSIUS, None - ) - sensor6.hass = hass - sensor6.entity_id = "input_number.target_temperature" - await sensor6.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -237,32 +175,10 @@ async def test_sensor_unit(hass, hass_client): 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body ) - assert ( - 'input_number_state_celsius{domain="input_number",' - 'entity="input_number.target_temperature",' - 'friendly_name="Target temperature"} 22.7' in body - ) - -async def test_sensor_without_unit(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_sensor_without_unit(client, sensor_entities): """Test prometheus metrics for sensors without a unit.""" - client = await setup_prometheus_client(hass, hass_client, "") - - sensor6 = DemoSensor(None, "Trend Gradient", 0.002, None, None, None, None) - sensor6.hass = hass - sensor6.entity_id = "sensor.trend_gradient" - await sensor6.async_update_ha_state() - - sensor7 = DemoSensor(None, "Text", "should_not_work", None, None, None, None) - sensor7.hass = hass - sensor7.entity_id = "sensor.text" - await sensor7.async_update_ha_state() - - sensor8 = DemoSensor(None, "Text Unit", "should_not_work", None, None, "Text", None) - sensor8.hass = hass - sensor8.entity_id = "sensor.text_unit" - await sensor8.async_update_ha_state() - body = await generate_latest_metrics(client) assert ( @@ -284,50 +200,9 @@ async def test_sensor_without_unit(hass, hass_client): ) -async def test_sensor_device_class(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_sensor_device_class(client, sensor_entities): """Test prometheus metrics for sensor with a device_class.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) - await hass.async_block_till_done() - - sensor1 = DemoSensor( - None, - "Fahrenheit", - 50, - SensorDeviceClass.TEMPERATURE, - None, - TEMP_FAHRENHEIT, - None, - ) - sensor1.hass = hass - sensor1.entity_id = "sensor.fahrenheit" - await sensor1.async_update_ha_state() - - sensor2 = DemoSensor( - None, - "Radio Energy", - 14, - SensorDeviceClass.POWER, - None, - ENERGY_KILO_WATT_HOUR, - None, - ) - sensor2.hass = hass - sensor2.entity_id = "sensor.radio_energy" - with mock.patch( - "homeassistant.util.dt.utcnow", - return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), - ): - await sensor2.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -355,27 +230,9 @@ async def test_sensor_device_class(hass, hass_client): ) -async def test_input_number(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_input_number(client, input_number_entities): """Test prometheus metrics for input_number.""" - client = await setup_prometheus_client(hass, hass_client, "") - - number1 = DemoNumber(None, "Threshold", 5.2, None, False, 0, 10, 0.1) - number1.hass = hass - number1.entity_id = "input_number.threshold" - await number1.async_update_ha_state() - - number2 = DemoNumber(None, None, 60, None, False, 0, 100) - number2.hass = hass - number2.entity_id = "input_number.brightness" - number2._attr_name = None - await number2.async_update_ha_state() - - number3 = DemoSensor(None, "Retry count", 5, None, None, None, None) - number3.hass = hass - number3.entity_id = "input_number.retry_count" - await number3.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -391,25 +248,15 @@ async def test_input_number(hass, hass_client): ) assert ( - 'input_number_state{domain="input_number",' - 'entity="input_number.retry_count",' - 'friendly_name="Retry count"} 5.0' in body + 'input_number_state_celsius{domain="input_number",' + 'entity="input_number.target_temperature",' + 'friendly_name="Target temperature"} 22.7' in body ) -async def test_battery(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_battery(client, sensor_entities): """Test prometheus metrics for battery.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component(hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]}) - await hass.async_block_till_done() - body = await generate_latest_metrics(client) assert ( @@ -419,21 +266,9 @@ async def test_battery(hass, hass_client): ) -async def test_climate(hass, hass_client): - """Test prometheus metrics for climate.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - await hass.async_block_till_done() +@pytest.mark.parametrize("namespace", [""]) +async def test_climate(client, climate_entities): + """Test prometheus metrics for climate entities.""" body = await generate_latest_metrics(client) assert ( @@ -460,36 +295,10 @@ async def test_climate(hass, hass_client): 'friendly_name="Ecobee"} 24.0' in body ) - assert ( - 'climate_mode{domain="climate",' - 'entity="climate.heatpump",' - 'friendly_name="HeatPump",' - 'mode="heat"} 1.0' in body - ) - assert ( - 'climate_mode{domain="climate",' - 'entity="climate.heatpump",' - 'friendly_name="HeatPump",' - 'mode="off"} 0.0' in body - ) - - -async def test_humidifier(hass, hass_client): - """Test prometheus metrics for battery.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component( - hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} - ) - - await hass.async_block_till_done() +@pytest.mark.parametrize("namespace", [""]) +async def test_humidifier(client, humidifier_entities): + """Test prometheus metrics for humidifier entities.""" body = await generate_latest_metrics(client) assert ( @@ -518,29 +327,15 @@ async def test_humidifier(hass, hass_client): ) -async def test_attributes(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_attributes(client, switch_entities): """Test prometheus metrics for entity attributes.""" - client = await setup_prometheus_client(hass, hass_client, "") - - switch1 = DemoSensor(None, "Boolean", 74, None, None, None, None) - switch1.hass = hass - switch1.entity_id = "switch.boolean" - switch1._attr_extra_state_attributes = {"boolean": True} - await switch1.async_update_ha_state() - - switch2 = DemoSensor(None, "Number", 42, None, None, None, None) - switch2.hass = hass - switch2.entity_id = "switch.number" - switch2._attr_extra_state_attributes = {"Number": 10.2} - await switch2.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( 'switch_state{domain="switch",' 'entity="switch.boolean",' - 'friendly_name="Boolean"} 74.0' in body + 'friendly_name="Boolean"} 1.0' in body ) assert ( @@ -552,7 +347,7 @@ async def test_attributes(hass, hass_client): assert ( 'switch_state{domain="switch",' 'entity="switch.number",' - 'friendly_name="Number"} 42.0' in body + 'friendly_name="Number"} 0.0' in body ) assert ( @@ -562,21 +357,9 @@ async def test_attributes(hass, hass_client): ) -async def test_binary_sensor(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_binary_sensor(client, binary_sensor_entities): """Test prometheus metrics for binary_sensor.""" - client = await setup_prometheus_client(hass, hass_client, "") - - binary_sensor1 = DemoBinarySensor(None, "Door", True, None) - binary_sensor1.hass = hass - binary_sensor1.entity_id = "binary_sensor.door" - await binary_sensor1.async_update_ha_state() - - binary_sensor1 = DemoBinarySensor(None, "Window", False, None) - binary_sensor1.hass = hass - binary_sensor1.entity_id = "binary_sensor.window" - await binary_sensor1.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -592,21 +375,9 @@ async def test_binary_sensor(hass, hass_client): ) -async def test_input_boolean(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_input_boolean(client, input_boolean_entities): """Test prometheus metrics for input_boolean.""" - client = await setup_prometheus_client(hass, hass_client, "") - - input_boolean1 = DemoSensor(None, "Test", 1, None, None, None, None) - input_boolean1.hass = hass - input_boolean1.entity_id = "input_boolean.test" - await input_boolean1.async_update_ha_state() - - input_boolean2 = DemoSensor(None, "Helper", 0, None, None, None, None) - input_boolean2.hass = hass - input_boolean2.entity_id = "input_boolean.helper" - await input_boolean2.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -622,31 +393,9 @@ async def test_input_boolean(hass, hass_client): ) -async def test_light(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_light(client, light_entities): """Test prometheus metrics for lights.""" - client = await setup_prometheus_client(hass, hass_client, "") - - light1 = DemoSensor(None, "Desk", 1, None, None, None, None) - light1.hass = hass - light1.entity_id = "light.desk" - await light1.async_update_ha_state() - - light2 = DemoSensor(None, "Wall", 0, None, None, None, None) - light2.hass = hass - light2.entity_id = "light.wall" - await light2.async_update_ha_state() - - light3 = DemoLight(None, "TV", True, True, 255, None, None) - light3.hass = hass - light3.entity_id = "light.tv" - await light3.async_update_ha_state() - - light4 = DemoLight(None, "PC", True, True, 180, None, None) - light4.hass = hass - light4.entity_id = "light.pc" - await light4.async_update_ha_state() - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -674,19 +423,9 @@ async def test_light(hass, hass_client): ) -async def test_lock(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_lock(client, lock_entities): """Test prometheus metrics for lock.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]}) - - await hass.async_block_till_done() body = await generate_latest_metrics(client) assert ( @@ -702,22 +441,9 @@ async def test_lock(hass, hass_client): ) -async def test_counter(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_counter(client, counter_entities): """Test prometheus metrics for counter.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component( - hass, counter.DOMAIN, {"counter": {"counter": {"initial": "2"}}} - ) - - await hass.async_block_till_done() - body = await generate_latest_metrics(client) assert ( @@ -727,24 +453,12 @@ async def test_counter(hass, hass_client): ) -async def test_renaming_entity_name(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_renaming_entity_name( + hass, registry, client, sensor_entities, climate_entities +): """Test renaming entity name.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - client = await setup_prometheus_client(hass, hass_client, "") - - assert await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) - - await hass.async_block_till_done() + data = {**sensor_entities, **climate_entities} body = await generate_latest_metrics(client) assert ( @@ -785,17 +499,29 @@ async def test_renaming_entity_name(hass, hass_client): 'friendly_name="HeatPump"} 0.0' in body ) - registry = entity_registry.async_get(hass) assert "sensor.outside_temperature" in registry.entities assert "climate.heatpump" in registry.entities registry.async_update_entity( - entity_id="sensor.outside_temperature", + entity_id=data["sensor_1"].entity_id, name="Outside Temperature Renamed", ) + set_state_with_entry( + hass, + data["sensor_1"], + 15.6, + {ATTR_FRIENDLY_NAME: "Outside Temperature Renamed"}, + ) registry.async_update_entity( - entity_id="climate.heatpump", + entity_id=data["climate_1"].entity_id, name="HeatPump Renamed", ) + data["climate_1_attributes"] = { + **data["climate_1_attributes"], + ATTR_FRIENDLY_NAME: "HeatPump Renamed", + } + set_state_with_entry( + hass, data["climate_1"], CURRENT_HVAC_HEAT, data["climate_1_attributes"] + ) await hass.async_block_till_done() body = await generate_latest_metrics(client) @@ -846,20 +572,12 @@ async def test_renaming_entity_name(hass, hass_client): ) -async def test_renaming_entity_id(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_renaming_entity_id( + hass, registry, client, sensor_entities, climate_entities +): """Test renaming entity id.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - client = await setup_prometheus_client(hass, hass_client, "") - - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) - - await hass.async_block_till_done() + data = {**sensor_entities, **climate_entities} body = await generate_latest_metrics(client) assert ( @@ -886,12 +604,15 @@ async def test_renaming_entity_id(hass, hass_client): 'friendly_name="Outside Humidity"} 1.0' in body ) - registry = entity_registry.async_get(hass) assert "sensor.outside_temperature" in registry.entities + assert "climate.heatpump" in registry.entities registry.async_update_entity( entity_id="sensor.outside_temperature", new_entity_id="sensor.outside_temperature_renamed", ) + set_state_with_entry( + hass, data["sensor_1"], 15.6, None, "sensor.outside_temperature_renamed" + ) await hass.async_block_till_done() body = await generate_latest_metrics(client) @@ -927,24 +648,12 @@ async def test_renaming_entity_id(hass, hass_client): ) -async def test_deleting_entity(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_deleting_entity( + hass, registry, client, sensor_entities, climate_entities +): """Test deleting a entity.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) - - await hass.async_block_till_done() + data = {**sensor_entities, **climate_entities} body = await generate_latest_metrics(client) assert ( @@ -985,11 +694,10 @@ async def test_deleting_entity(hass, hass_client): 'friendly_name="HeatPump"} 0.0' in body ) - registry = entity_registry.async_get(hass) assert "sensor.outside_temperature" in registry.entities assert "climate.heatpump" in registry.entities - registry.async_remove("sensor.outside_temperature") - registry.async_remove("climate.heatpump") + registry.async_remove(data["sensor_1"].entity_id) + registry.async_remove(data["climate_1"].entity_id) await hass.async_block_till_done() body = await generate_latest_metrics(client) @@ -1015,22 +723,12 @@ async def test_deleting_entity(hass, hass_client): ) -async def test_disabling_entity(hass, hass_client): +@pytest.mark.parametrize("namespace", [""]) +async def test_disabling_entity( + hass, registry, client, sensor_entities, climate_entities +): """Test disabling a entity.""" - assert await async_setup_component( - hass, - "conversation", - {}, - ) - client = await setup_prometheus_client(hass, hass_client, "") - - await async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - - assert await async_setup_component( - hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} - ) + data = {**sensor_entities, **climate_entities} await hass.async_block_till_done() body = await generate_latest_metrics(client) @@ -1080,11 +778,10 @@ async def test_disabling_entity(hass, hass_client): 'friendly_name="HeatPump"} 0.0' in body ) - registry = entity_registry.async_get(hass) assert "sensor.outside_temperature" in registry.entities assert "climate.heatpump" in registry.entities registry.async_update_entity( - entity_id="sensor.outside_temperature", + entity_id=data["sensor_1"].entity_id, disabled_by="user", ) registry.async_update_entity(entity_id="climate.heatpump", disabled_by="user") @@ -1113,6 +810,553 @@ async def test_disabling_entity(hass, hass_client): ) +@pytest.fixture(name="registry") +def entity_registry_fixture(hass): + """Provide entity registry.""" + return entity_registry.async_get(hass) + + +@pytest.fixture(name="sensor_entities") +async def sensor_fixture(hass, registry): + """Simulate sensor entities.""" + data = {} + sensor_1 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_1", + unit_of_measurement=TEMP_CELSIUS, + original_device_class=SensorDeviceClass.TEMPERATURE, + suggested_object_id="outside_temperature", + original_name="Outside Temperature", + ) + sensor_1_attributes = {ATTR_BATTERY_LEVEL: 12} + set_state_with_entry(hass, sensor_1, 15.6, sensor_1_attributes) + data["sensor_1"] = sensor_1 + data["sensor_1_attributes"] = sensor_1_attributes + + sensor_2 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_2", + unit_of_measurement=PERCENTAGE, + original_device_class=SensorDeviceClass.HUMIDITY, + suggested_object_id="outside_humidity", + original_name="Outside Humidity", + ) + set_state_with_entry(hass, sensor_2, 54.0) + data["sensor_2"] = sensor_2 + + sensor_3 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_3", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + original_device_class=SensorDeviceClass.POWER, + suggested_object_id="radio_energy", + original_name="Radio Energy", + ) + with mock.patch( + "homeassistant.util.dt.utcnow", + return_value=datetime.datetime(1970, 1, 2, tzinfo=dt_util.UTC), + ): + set_state_with_entry(hass, sensor_3, 14) + data["sensor_3"] = sensor_3 + + sensor_4 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_4", + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + suggested_object_id="television_energy", + original_name="Television Energy", + ) + set_state_with_entry(hass, sensor_4, 74) + data["sensor_4"] = sensor_4 + + sensor_5 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_5", + unit_of_measurement=f"SEK/{ENERGY_KILO_WATT_HOUR}", + suggested_object_id="electricity_price", + original_name="Electricity price", + ) + set_state_with_entry(hass, sensor_5, 0.123) + data["sensor_5"] = sensor_5 + + sensor_6 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_6", + unit_of_measurement=DEGREE, + suggested_object_id="wind_direction", + original_name="Wind Direction", + ) + set_state_with_entry(hass, sensor_6, 25) + data["sensor_6"] = sensor_6 + + sensor_7 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_7", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + suggested_object_id="sps30_pm_1um_weight_concentration", + original_name="SPS30 PM <1µm Weight concentration", + ) + set_state_with_entry(hass, sensor_7, 3.7069) + data["sensor_7"] = sensor_7 + + sensor_8 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_8", + suggested_object_id="trend_gradient", + original_name="Trend Gradient", + ) + set_state_with_entry(hass, sensor_8, 0.002) + data["sensor_8"] = sensor_8 + + sensor_9 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_9", + suggested_object_id="text", + original_name="Text", + ) + set_state_with_entry(hass, sensor_9, "should_not_work") + data["sensor_9"] = sensor_9 + + sensor_10 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_10", + unit_of_measurement="Text", + suggested_object_id="text_unit", + original_name="Text Unit", + ) + set_state_with_entry(hass, sensor_10, "should_not_work") + data["sensor_10"] = sensor_10 + + sensor_11 = registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_11", + unit_of_measurement=TEMP_FAHRENHEIT, + original_device_class=SensorDeviceClass.TEMPERATURE, + suggested_object_id="fahrenheit", + original_name="Fahrenheit", + ) + set_state_with_entry(hass, sensor_11, 50) + data["sensor_11"] = sensor_11 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="climate_entities") +async def climate_fixture(hass, registry): + """Simulate climate entities.""" + data = {} + climate_1 = registry.async_get_or_create( + domain=climate.DOMAIN, + platform="test", + unique_id="climate_1", + unit_of_measurement=TEMP_CELSIUS, + suggested_object_id="heatpump", + original_name="HeatPump", + ) + climate_1_attributes = { + ATTR_TEMPERATURE: 20, + ATTR_CURRENT_TEMPERATURE: 25, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, + } + set_state_with_entry(hass, climate_1, CURRENT_HVAC_HEAT, climate_1_attributes) + data["climate_1"] = climate_1 + data["climate_1_attributes"] = climate_1_attributes + + climate_2 = registry.async_get_or_create( + domain=climate.DOMAIN, + platform="test", + unique_id="climate_2", + unit_of_measurement=TEMP_CELSIUS, + suggested_object_id="ecobee", + original_name="Ecobee", + ) + climate_2_attributes = { + ATTR_TEMPERATURE: 21, + ATTR_CURRENT_TEMPERATURE: 22, + ATTR_TARGET_TEMP_LOW: 21, + ATTR_TARGET_TEMP_HIGH: 24, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + } + set_state_with_entry(hass, climate_2, CURRENT_HVAC_HEAT, climate_2_attributes) + data["climate_2"] = climate_2 + data["climate_2_attributes"] = climate_2_attributes + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="humidifier_entities") +async def humidifier_fixture(hass, registry): + """Simulate humidifier entities.""" + data = {} + humidifier_1 = registry.async_get_or_create( + domain=humidifier.DOMAIN, + platform="test", + unique_id="humidifier_1", + original_device_class=humidifier.HumidifierDeviceClass.HUMIDIFIER, + suggested_object_id="humidifier", + original_name="Humidifier", + ) + humidifier_1_attributes = { + ATTR_HUMIDITY: 68, + } + set_state_with_entry(hass, humidifier_1, STATE_ON, humidifier_1_attributes) + data["humidifier_1"] = humidifier_1 + data["humidifier_1_attributes"] = humidifier_1_attributes + + humidifier_2 = registry.async_get_or_create( + domain=humidifier.DOMAIN, + platform="test", + unique_id="humidifier_2", + original_device_class=humidifier.HumidifierDeviceClass.DEHUMIDIFIER, + suggested_object_id="dehumidifier", + original_name="Dehumidifier", + ) + humidifier_2_attributes = { + ATTR_HUMIDITY: 54, + } + set_state_with_entry(hass, humidifier_2, STATE_ON, humidifier_2_attributes) + data["humidifier_2"] = humidifier_2 + data["humidifier_2_attributes"] = humidifier_2_attributes + + humidifier_3 = registry.async_get_or_create( + domain=humidifier.DOMAIN, + platform="test", + unique_id="humidifier_3", + suggested_object_id="hygrostat", + original_name="Hygrostat", + ) + humidifier_3_attributes = { + ATTR_HUMIDITY: 50, + ATTR_MODE: "home", + ATTR_AVAILABLE_MODES: ["home", "eco"], + } + set_state_with_entry(hass, humidifier_3, STATE_ON, humidifier_3_attributes) + data["humidifier_3"] = humidifier_3 + data["humidifier_3_attributes"] = humidifier_3_attributes + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="lock_entities") +async def lock_fixture(hass, registry): + """Simulate lock entities.""" + data = {} + lock_1 = registry.async_get_or_create( + domain=lock.DOMAIN, + platform="test", + unique_id="lock_1", + suggested_object_id="front_door", + original_name="Front Door", + ) + set_state_with_entry(hass, lock_1, STATE_LOCKED) + data["lock_1"] = lock_1 + + lock_2 = registry.async_get_or_create( + domain=lock.DOMAIN, + platform="test", + unique_id="lock_2", + suggested_object_id="kitchen_door", + original_name="Kitchen Door", + ) + set_state_with_entry(hass, lock_2, STATE_UNLOCKED) + data["lock_2"] = lock_2 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="input_number_entities") +async def input_number_fixture(hass, registry): + """Simulate input_number entities.""" + data = {} + input_number_1 = registry.async_get_or_create( + domain=input_number.DOMAIN, + platform="test", + unique_id="input_number_1", + suggested_object_id="threshold", + original_name="Threshold", + ) + set_state_with_entry(hass, input_number_1, 5.2) + data["input_number_1"] = input_number_1 + + input_number_2 = registry.async_get_or_create( + domain=input_number.DOMAIN, + platform="test", + unique_id="input_number_2", + suggested_object_id="brightness", + ) + set_state_with_entry(hass, input_number_2, 60) + data["input_number_2"] = input_number_2 + + input_number_3 = registry.async_get_or_create( + domain=input_number.DOMAIN, + platform="test", + unique_id="input_number_3", + suggested_object_id="target_temperature", + original_name="Target temperature", + unit_of_measurement=TEMP_CELSIUS, + ) + set_state_with_entry(hass, input_number_3, 22.7) + data["input_number_3"] = input_number_3 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="input_boolean_entities") +async def input_boolean_fixture(hass, registry): + """Simulate input_boolean entities.""" + data = {} + input_boolean_1 = registry.async_get_or_create( + domain=input_boolean.DOMAIN, + platform="test", + unique_id="input_boolean_1", + suggested_object_id="test", + original_name="Test", + ) + set_state_with_entry(hass, input_boolean_1, STATE_ON) + data["input_boolean_1"] = input_boolean_1 + + input_boolean_2 = registry.async_get_or_create( + domain=input_boolean.DOMAIN, + platform="test", + unique_id="input_boolean_2", + suggested_object_id="helper", + original_name="Helper", + ) + set_state_with_entry(hass, input_boolean_2, STATE_OFF) + data["input_boolean_2"] = input_boolean_2 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="binary_sensor_entities") +async def binary_sensor_fixture(hass, registry): + """Simulate binary_sensor entities.""" + data = {} + binary_sensor_1 = registry.async_get_or_create( + domain=binary_sensor.DOMAIN, + platform="test", + unique_id="binary_sensor_1", + suggested_object_id="door", + original_name="Door", + ) + set_state_with_entry(hass, binary_sensor_1, STATE_ON) + data["binary_sensor_1"] = binary_sensor_1 + + binary_sensor_2 = registry.async_get_or_create( + domain=binary_sensor.DOMAIN, + platform="test", + unique_id="binary_sensor_2", + suggested_object_id="window", + original_name="Window", + ) + set_state_with_entry(hass, binary_sensor_2, STATE_OFF) + data["binary_sensor_2"] = binary_sensor_2 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="light_entities") +async def light_fixture(hass, registry): + """Simulate light entities.""" + data = {} + light_1 = registry.async_get_or_create( + domain=light.DOMAIN, + platform="test", + unique_id="light_1", + suggested_object_id="desk", + original_name="Desk", + ) + set_state_with_entry(hass, light_1, STATE_ON) + data["light_1"] = light_1 + + light_2 = registry.async_get_or_create( + domain=light.DOMAIN, + platform="test", + unique_id="light_2", + suggested_object_id="wall", + original_name="Wall", + ) + set_state_with_entry(hass, light_2, STATE_OFF) + data["light_2"] = light_2 + + light_3 = registry.async_get_or_create( + domain=light.DOMAIN, + platform="test", + unique_id="light_3", + suggested_object_id="tv", + original_name="TV", + ) + light_3_attributes = {light.ATTR_BRIGHTNESS: 255} + set_state_with_entry(hass, light_3, STATE_ON, light_3_attributes) + data["light_3"] = light_3 + data["light_3_attributes"] = light_3_attributes + + light_4 = registry.async_get_or_create( + domain=light.DOMAIN, + platform="test", + unique_id="light_4", + suggested_object_id="pc", + original_name="PC", + ) + light_4_attributes = {light.ATTR_BRIGHTNESS: 180} + set_state_with_entry(hass, light_4, STATE_ON, light_4_attributes) + data["light_4"] = light_4 + data["light_4_attributes"] = light_4_attributes + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="switch_entities") +async def switch_fixture(hass, registry): + """Simulate switch entities.""" + data = {} + switch_1 = registry.async_get_or_create( + domain=switch.DOMAIN, + platform="test", + unique_id="switch_1", + suggested_object_id="boolean", + original_name="Boolean", + ) + switch_1_attributes = {"boolean": True} + set_state_with_entry(hass, switch_1, STATE_ON, switch_1_attributes) + data["switch_1"] = switch_1 + data["switch_1_attributes"] = switch_1_attributes + + switch_2 = registry.async_get_or_create( + domain=switch.DOMAIN, + platform="test", + unique_id="switch_2", + suggested_object_id="number", + original_name="Number", + ) + switch_2_attributes = {"Number": 10.2} + set_state_with_entry(hass, switch_2, STATE_OFF, switch_2_attributes) + data["switch_2"] = switch_2 + data["switch_2_attributes"] = switch_2_attributes + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="person_entities") +async def person_fixture(hass, registry): + """Simulate person entities.""" + data = {} + person_1 = registry.async_get_or_create( + domain=person.DOMAIN, + platform="test", + unique_id="person_1", + suggested_object_id="bob", + original_name="Bob", + ) + set_state_with_entry(hass, person_1, STATE_HOME) + data["person_1"] = person_1 + + person_2 = registry.async_get_or_create( + domain=person.DOMAIN, + platform="test", + unique_id="person_2", + suggested_object_id="alice", + original_name="Alice", + ) + set_state_with_entry(hass, person_2, STATE_NOT_HOME) + data["person_2"] = person_2 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="device_tracker_entities") +async def device_tracker_fixture(hass, registry): + """Simulate device_tracker entities.""" + data = {} + device_tracker_1 = registry.async_get_or_create( + domain=device_tracker.DOMAIN, + platform="test", + unique_id="device_tracker_1", + suggested_object_id="phone", + original_name="Phone", + ) + set_state_with_entry(hass, device_tracker_1, STATE_HOME) + data["device_tracker_1"] = device_tracker_1 + + device_tracker_2 = registry.async_get_or_create( + domain=device_tracker.DOMAIN, + platform="test", + unique_id="device_tracker_2", + suggested_object_id="watch", + original_name="Watch", + ) + set_state_with_entry(hass, device_tracker_2, STATE_NOT_HOME) + data["device_tracker_2"] = device_tracker_2 + + await hass.async_block_till_done() + return data + + +@pytest.fixture(name="counter_entities") +async def counter_fixture(hass, registry): + """Simulate counter entities.""" + data = {} + counter_1 = registry.async_get_or_create( + domain=counter.DOMAIN, + platform="test", + unique_id="counter_1", + suggested_object_id="counter", + ) + set_state_with_entry(hass, counter_1, 2) + data["counter_1"] = counter_1 + + await hass.async_block_till_done() + return data + + +def set_state_with_entry( + hass, + entry: entity_registry.RegistryEntry, + state, + additional_attributes=None, + new_entity_id=None, +): + """Set the state of an entity with an Entity Registry entry.""" + attributes = {} + + if entry.original_name: + attributes[ATTR_FRIENDLY_NAME] = entry.original_name + if entry.unit_of_measurement: + attributes[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + if entry.original_device_class: + attributes[ATTR_DEVICE_CLASS] = entry.original_device_class + + if additional_attributes: + attributes = {**attributes, **additional_attributes} + + hass.states.async_set( + entity_id=new_entity_id if new_entity_id else entry.entity_id, + new_state=state, + attributes=attributes, + ) + + @pytest.fixture(name="mock_client") def mock_client_fixture(): """Mock the prometheus client.""" diff --git a/tests/components/pure_energie/__init__.py b/tests/components/pure_energie/__init__.py new file mode 100644 index 0000000000000..ee7ccbfb483a1 --- /dev/null +++ b/tests/components/pure_energie/__init__.py @@ -0,0 +1 @@ +"""Tests for the Pure Energie integration.""" diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py new file mode 100644 index 0000000000000..4bb89860ce3d8 --- /dev/null +++ b/tests/components/pure_energie/conftest.py @@ -0,0 +1,83 @@ +"""Fixtures for Pure Energie integration tests.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from gridnet import Device as GridNetDevice, SmartBridge +import pytest + +from homeassistant.components.pure_energie.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="home", + domain=DOMAIN, + data={CONF_HOST: "192.168.1.123"}, + unique_id="unique_thingy", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.pure_energie.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture +def mock_pure_energie_config_flow( + request: pytest.FixtureRequest, +) -> Generator[None, MagicMock, None]: + """Return a mocked Pure Energie client.""" + with patch( + "homeassistant.components.pure_energie.config_flow.GridNet", autospec=True + ) as pure_energie_mock: + pure_energie = pure_energie_mock.return_value + pure_energie.device.return_value = GridNetDevice.from_dict( + json.loads(load_fixture("device.json", DOMAIN)) + ) + yield pure_energie + + +@pytest.fixture +def mock_pure_energie(): + """Return a mocked Pure Energie client.""" + with patch( + "homeassistant.components.pure_energie.GridNet", autospec=True + ) as pure_energie_mock: + pure_energie = pure_energie_mock.return_value + pure_energie.smartbridge = AsyncMock( + return_value=SmartBridge.from_dict( + json.loads(load_fixture("pure_energie/smartbridge.json")) + ) + ) + pure_energie.device = AsyncMock( + return_value=GridNetDevice.from_dict( + json.loads(load_fixture("pure_energie/device.json")) + ) + ) + yield pure_energie_mock + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pure_energie: MagicMock, +) -> MockConfigEntry: + """Set up the Pure Energie integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/pure_energie/fixtures/device.json b/tests/components/pure_energie/fixtures/device.json new file mode 100644 index 0000000000000..3580d4066ac2b --- /dev/null +++ b/tests/components/pure_energie/fixtures/device.json @@ -0,0 +1 @@ +{"id":"aabbccddeeff","mf":"NET2GRID","model":"SBWF3102","fw":"1.6.16","hw":1,"batch":"SBP-HMX-210318"} \ No newline at end of file diff --git a/tests/components/pure_energie/fixtures/smartbridge.json b/tests/components/pure_energie/fixtures/smartbridge.json new file mode 100644 index 0000000000000..a0268d666ba89 --- /dev/null +++ b/tests/components/pure_energie/fixtures/smartbridge.json @@ -0,0 +1 @@ +{"status":"ok","elec":{"power":{"now":{"value":338,"unit":"W","time":1634749148},"min":{"value":-7345,"unit":"W","time":1631360893},"max":{"value":13725,"unit":"W","time":1633749513}},"import":{"now":{"value":17762055,"unit":"Wh","time":1634749148}},"export":{"now":{"value":21214589,"unit":"Wh","time":1634749148}}},"gas":{}} \ No newline at end of file diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py new file mode 100644 index 0000000000000..441a5977a2db9 --- /dev/null +++ b/tests/components/pure_energie/test_config_flow.py @@ -0,0 +1,123 @@ +"""Test the Pure Energie config flow.""" +from unittest.mock import MagicMock + +from gridnet import GridNetConnectionError + +from homeassistant.components import zeroconf +from homeassistant.components.pure_energie.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, + mock_pure_energie_config_flow: MagicMock, + mock_setup_entry: None, +) -> None: + """Test the full manual user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result.get("step_id") == SOURCE_USER + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} + ) + + assert result.get("title") == "Pure Energie Meter" + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert "data" in result + assert result["data"][CONF_HOST] == "192.168.1.123" + assert "result" in result + assert result["result"].unique_id == "aabbccddeeff" + + +async def test_full_zeroconf_flow_implementationn( + hass: HomeAssistant, + mock_pure_energie_config_flow: MagicMock, + mock_setup_entry: None, +) -> None: + """Test the full manual user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + addresses=["192.168.1.123"], + hostname="example.local.", + name="mock_name", + port=None, + properties={CONF_MAC: "aabbccddeeff"}, + type="mock_type", + ), + ) + + assert result.get("description_placeholders") == { + "model": "SBWF3102", + CONF_NAME: "Pure Energie Meter", + } + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2.get("title") == "Pure Energie Meter" + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + + assert "data" in result2 + assert result2["data"][CONF_HOST] == "192.168.1.123" + assert "result" in result2 + assert result2["result"].unique_id == "aabbccddeeff" + + +async def test_connection_error( + hass: HomeAssistant, mock_pure_energie_config_flow: MagicMock +) -> None: + """Test we show user form on Pure Energie connection error.""" + mock_pure_energie_config_flow.device.side_effect = GridNetConnectionError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.com"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + +async def test_zeroconf_connection_error( + hass: HomeAssistant, mock_pure_energie_config_flow: MagicMock +) -> None: + """Test we abort zeroconf flow on Pure Energie connection error.""" + mock_pure_energie_config_flow.device.side_effect = GridNetConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + addresses=["192.168.1.123"], + hostname="example.local.", + name="mock_name", + port=None, + properties={CONF_MAC: "aabbccddeeff"}, + type="mock_type", + ), + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" diff --git a/tests/components/pure_energie/test_init.py b/tests/components/pure_energie/test_init.py new file mode 100644 index 0000000000000..c5ffc19c64097 --- /dev/null +++ b/tests/components/pure_energie/test_init.py @@ -0,0 +1,52 @@ +"""Tests for the Pure Energie integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from gridnet import GridNetConnectionError +import pytest + +from homeassistant.components.pure_energie.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "mock_pure_energie", ["pure_energie/device.json"], indirect=True +) +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pure_energie: AsyncMock, +) -> None: + """Test the Pure Energie configuration entry loading/unloading.""" + 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 mock_config_entry.state is ConfigEntryState.LOADED + assert mock_config_entry.unique_id == "unique_thingy" + assert len(mock_pure_energie.mock_calls) == 3 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.pure_energie.GridNet.request", + side_effect=GridNetConnectionError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Pure Energie configuration entry not ready.""" + 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 mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/pure_energie/test_sensor.py b/tests/components/pure_energie/test_sensor.py new file mode 100644 index 0000000000000..60894ac09f877 --- /dev/null +++ b/tests/components/pure_energie/test_sensor.py @@ -0,0 +1,74 @@ +"""Tests for the sensors provided by the Pure Energie integration.""" + +from homeassistant.components.pure_energie.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Pure Energie - SmartBridge sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.pem_energy_consumption_total") + entry = entity_registry.async_get("sensor.pem_energy_consumption_total") + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_energy_consumption_total" + assert state.state == "17762.1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.pem_energy_production_total") + entry = entity_registry.async_get("sensor.pem_energy_production_total") + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_energy_production_total" + assert state.state == "21214.6" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.pem_power_flow") + entry = entity_registry.async_get("sensor.pem_power_flow") + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_power_flow" + assert state.state == "338" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Flow" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert ATTR_ICON not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "aabbccddeeff")} + assert device_entry.name == "home" + assert device_entry.manufacturer == "NET2GRID" + assert device_entry.model == "SBWF3102" + assert device_entry.sw_version == "1.6.16" diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index cf9e811ed5a3c..00445f23c01ed 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -114,6 +114,7 @@ async def test_form_homekit(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, @@ -138,6 +139,7 @@ async def test_form_homekit(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/radio_browser/__init__.py b/tests/components/radio_browser/__init__.py new file mode 100644 index 0000000000000..708e07fefe659 --- /dev/null +++ b/tests/components/radio_browser/__init__.py @@ -0,0 +1 @@ +"""Tests for the Radio Browser integration.""" diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py new file mode 100644 index 0000000000000..5a5b888d944a6 --- /dev/null +++ b/tests/components/radio_browser/conftest.py @@ -0,0 +1,30 @@ +"""Fixtures for the Radio Browser integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.radio_browser.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My Radios", + domain=DOMAIN, + data={}, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.radio_browser.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py new file mode 100644 index 0000000000000..8a5a3d9ccce9d --- /dev/null +++ b/tests/components/radio_browser/test_config_flow.py @@ -0,0 +1,65 @@ +"""Test the Radio Browser config flow.""" +from unittest.mock import AsyncMock + +from homeassistant.components.radio_browser.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_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} + ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") is None + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Radio Browser" + assert result2.get("data") == {} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test we abort if the Radio Browser is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_onboarding_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test the onboarding configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "onboarding"} + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Radio Browser" + assert result.get("data") == {} + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 4514bbfb9d856..5a0e1fc08cc67 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -149,6 +149,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], hostname="mock_hostname", name="mock_name", port=None, @@ -174,6 +175,7 @@ async def test_step_homekit_zeroconf_ip_change(hass, client, config_entry, sourc context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.2", + addresses=["192.168.1.2"], hostname="mock_hostname", name="mock_name", port=None, @@ -202,6 +204,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( context={"source": source}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], hostname="mock_hostname", name="mock_name", port=None, @@ -249,6 +252,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], hostname="mock_hostname", name="mock_name", port=None, @@ -268,6 +272,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.100", + addresses=["192.168.1.100"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py index a1f3b42167c1e..6c62e5d22e2ee 100644 --- a/tests/components/renault/conftest.py +++ b/tests/components/renault/conftest.py @@ -63,12 +63,6 @@ def patch_get_vehicles(vehicle_type: str): load_fixture(f"renault/vehicle_{vehicle_type}.json") ) ), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", - side_effect=MOCK_VEHICLES[vehicle_type]["endpoints_available"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", - return_value=True, ): yield @@ -102,6 +96,16 @@ def _get_fixtures(vehicle_type: str) -> MappingProxyType: if "location" in mock_vehicle["endpoints"] else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleLocationDataSchema), + "lock_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['lock_status']}") + if "lock_status" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleLockStatusDataSchema), + "res_state": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['res_state']}") + if "res_state" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleResStateDataSchema), } @@ -125,6 +129,12 @@ def patch_fixtures_with_data(vehicle_type: str): ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_location", return_value=mock_fixtures["location"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + return_value=mock_fixtures["lock_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + return_value=mock_fixtures["res_state"], ): yield @@ -149,6 +159,12 @@ def patch_fixtures_with_no_data(): ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_location", return_value=mock_fixtures["location"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + return_value=mock_fixtures["lock_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + return_value=mock_fixtures["res_state"], ): yield @@ -171,6 +187,12 @@ def _patch_fixtures_with_side_effect(side_effect: Any): ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_location", side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + side_effect=side_effect, ): yield diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 91704a59b5178..41a72c6b7ab37 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -54,6 +54,7 @@ DYNAMIC_ATTRIBUTES = (ATTR_ICON,) ICON_FOR_EMPTY_VALUES = { + "binary_sensor.reg_number_hvac": "mdi:fan-off", "select.reg_number_charge_mode": "mdi:calendar-remove", "sensor.reg_number_charge_state": "mdi:flash-off", "sensor.reg_number_plug_state": "mdi:power-plug-off", @@ -78,18 +79,11 @@ ATTR_NAME: "REG-NUMBER", ATTR_SW_VERSION: "X101VE", }, - "endpoints_available": [ - True, # cockpit - True, # hvac-status - False, # location - True, # battery-status - True, # charge-mode - ], "endpoints": { "battery_status": "battery_status_charging.json", "charge_mode": "charge_mode_always.json", "cockpit": "cockpit_ev.json", - "hvac_status": "hvac_status.json", + "hvac_status": "hvac_status.1.json", }, Platform.BINARY_SENSOR: [ { @@ -104,6 +98,12 @@ ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, + { + ATTR_ENTITY_ID: "binary_sensor.reg_number_hvac", + ATTR_ICON: "mdi:fan-off", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_status", + }, ], Platform.BUTTON: [ { @@ -209,6 +209,19 @@ ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, + { + ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_soc_threshold", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_hvac_last_activity", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_last_activity", + }, { ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", @@ -216,6 +229,17 @@ ATTR_STATE: "plugged", ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state", }, + { + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_res_state", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start_code", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_res_state_code", + }, ], }, "zoe_50": { @@ -226,19 +250,14 @@ ATTR_NAME: "REG-NUMBER", ATTR_SW_VERSION: "X102VE", }, - "endpoints_available": [ - True, # cockpit - True, # hvac-status - True, # location - True, # battery-status - True, # charge-mode - ], "endpoints": { "battery_status": "battery_status_not_charging.json", "charge_mode": "charge_mode_schedule.json", "cockpit": "cockpit_ev.json", - "hvac_status": "hvac_status.json", + "hvac_status": "hvac_status.2.json", "location": "location.json", + "lock_status": "lock_status.1.json", + "res_state": "res_state.1.json", }, Platform.BINARY_SENSOR: [ { @@ -253,6 +272,48 @@ ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, + { + ATTR_ENTITY_ID: "binary_sensor.reg_number_hvac", + ATTR_ICON: "mdi:fan-off", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, + ATTR_ENTITY_ID: "binary_sensor.reg_number_lock", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_lock_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_left_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_rear_left_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_right_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_rear_right_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_driver_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_driver_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_passenger_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_passenger_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_hatch", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hatch_status", + }, ], Platform.BUTTON: [ { @@ -360,11 +421,24 @@ { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ATTR_ENTITY_ID: "sensor.reg_number_outside_temperature", - ATTR_STATE: "8.0", + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, + { + ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", + ATTR_STATE: "30.0", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_soc_threshold", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_hvac_last_activity", + ATTR_STATE: "2020-12-03T00:00:00+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_last_activity", + }, { ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ENTITY_ID: "sensor.reg_number_plug_state", @@ -379,6 +453,17 @@ ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777999_location_last_activity", }, + { + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start", + ATTR_STATE: "Stopped, ready for RES", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_res_state", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start_code", + ATTR_STATE: "10", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_res_state_code", + }, ], }, "captur_phev": { @@ -389,18 +474,13 @@ ATTR_NAME: "REG-NUMBER", ATTR_SW_VERSION: "XJB1SU", }, - "endpoints_available": [ - True, # cockpit - False, # hvac-status - True, # location - True, # battery-status - True, # charge-mode - ], "endpoints": { "battery_status": "battery_status_charging.json", "charge_mode": "charge_mode_always.json", "cockpit": "cockpit_fuel.json", "location": "location.json", + "lock_status": "lock_status.1.json", + "res_state": "res_state.1.json", }, Platform.BINARY_SENSOR: [ { @@ -415,6 +495,42 @@ ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging", }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, + ATTR_ENTITY_ID: "binary_sensor.reg_number_lock", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_lock_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_left_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_rear_left_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_right_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_rear_right_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_driver_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_driver_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_passenger_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_passenger_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_hatch", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_hatch_status", + }, ], Platform.BUTTON: [ { @@ -549,6 +665,17 @@ ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", }, + { + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start", + ATTR_STATE: "Stopped, ready for RES", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_res_state", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start_code", + ATTR_STATE: "10", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_res_state_code", + }, ], }, "captur_fuel": { @@ -559,18 +686,50 @@ ATTR_NAME: "REG-NUMBER", ATTR_SW_VERSION: "XJB1SU", }, - "endpoints_available": [ - True, # cockpit - False, # hvac-status - True, # location - # Ignore, # battery-status - # Ignore, # charge-mode - ], "endpoints": { "cockpit": "cockpit_fuel.json", "location": "location.json", + "lock_status": "lock_status.1.json", + "res_state": "res_state.1.json", }, - Platform.BINARY_SENSOR: [], + Platform.BINARY_SENSOR: [ + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, + ATTR_ENTITY_ID: "binary_sensor.reg_number_lock", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_lock_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_left_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_rear_left_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_rear_right_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_rear_right_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_driver_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_driver_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_passenger_door", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_passenger_door_status", + }, + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, + ATTR_ENTITY_ID: "binary_sensor.reg_number_hatch", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_hatch_status", + }, + ], Platform.BUTTON: [ { ATTR_ENTITY_ID: "button.reg_number_start_air_conditioner", @@ -620,6 +779,17 @@ ATTR_STATE: "2020-02-18T16:58:38+00:00", ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", }, + { + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start", + ATTR_STATE: "Stopped, ready for RES", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_res_state", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.reg_number_remote_engine_start_code", + ATTR_STATE: "10", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_res_state_code", + }, ], }, } diff --git a/tests/components/renault/fixtures/hvac_status.json b/tests/components/renault/fixtures/hvac_status.1.json similarity index 100% rename from tests/components/renault/fixtures/hvac_status.json rename to tests/components/renault/fixtures/hvac_status.1.json diff --git a/tests/components/renault/fixtures/hvac_status.2.json b/tests/components/renault/fixtures/hvac_status.2.json new file mode 100644 index 0000000000000..a2ca08a71e9ec --- /dev/null +++ b/tests/components/renault/fixtures/hvac_status.2.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "socThreshold": 30.0, + "hvacStatus": "off", + "lastUpdateTime": "2020-12-03T00:00:00Z" + } + } +} diff --git a/tests/components/renault/fixtures/lock_status.1.json b/tests/components/renault/fixtures/lock_status.1.json new file mode 100644 index 0000000000000..9cda30d5a626e --- /dev/null +++ b/tests/components/renault/fixtures/lock_status.1.json @@ -0,0 +1,15 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "lockStatus": "locked", + "doorStatusRearLeft": "closed", + "doorStatusRearRight": "closed", + "doorStatusDriver": "closed", + "doorStatusPassenger": "closed", + "hatchStatus": "closed", + "lastUpdateTime": "2022-02-02T13:51:13Z" + } + } + } \ No newline at end of file diff --git a/tests/components/renault/fixtures/res_state.1.json b/tests/components/renault/fixtures/res_state.1.json new file mode 100644 index 0000000000000..e6973ed091af8 --- /dev/null +++ b/tests/components/renault/fixtures/res_state.1.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "ResState", + "id": "VF1AAAAA555777999", + "attributes": { + "details": "Stopped, ready for RES", + "code": "10" + } + } + } \ No newline at end of file diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index 0a2460edca14a..feef06746bd20 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -4,7 +4,7 @@ import pytest from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, Platform +from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from . import ( @@ -63,7 +63,7 @@ async def test_binary_sensor_empty( expected_entities = mock_vehicle[Platform.BINARY_SENSOR] assert len(entity_registry.entities) == len(expected_entities) - check_entities_no_data(hass, entity_registry, expected_entities, STATE_OFF) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index cf6fc1902e911..6ed50a833f11a 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -4,7 +4,7 @@ import pytest from renault_api.kamereon import schemas -from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -150,7 +150,7 @@ async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntr ), ) as mock_action: await hass.services.async_call( - Platform.BUTTON, SERVICE_PRESS, service_data=data, blocking=True + BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () @@ -178,7 +178,7 @@ async def test_button_start_air_conditioner( ), ) as mock_action: await hass.services.async_call( - Platform.BUTTON, SERVICE_PRESS, service_data=data, blocking=True + BUTTON_DOMAIN, SERVICE_PRESS, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (21, None) diff --git a/tests/components/renault/test_diagnostics.py b/tests/components/renault/test_diagnostics.py index 1a61859ac93a5..fccf5a757b400 100644 --- a/tests/components/renault/test_diagnostics.py +++ b/tests/components/renault/test_diagnostics.py @@ -157,6 +157,7 @@ "externalTemperature": 8.0, "hvacStatus": "off", }, + "res_state": {}, } diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index e0cb4413a7e3e..18c2eed8a7c3f 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -4,7 +4,11 @@ import pytest from renault_api.kamereon import schemas -from homeassistant.components.select.const import ATTR_OPTION, SERVICE_SELECT_OPTION +from homeassistant.components.select.const import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant @@ -145,7 +149,7 @@ async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry ), ) as mock_action: await hass.services.async_call( - Platform.SELECT, SERVICE_SELECT_OPTION, service_data=data, blocking=True + SELECT_DOMAIN, SERVICE_SELECT_OPTION, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == ("always",) diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 4b6acfb43946f..00873e9d17666 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -15,10 +15,10 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -import homeassistant.core as ha +from homeassistant.core import CoreState, State, callback import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache from tests.components.rflink.test_init import mock_rflink DOMAIN = "binary_sensor" @@ -91,13 +91,19 @@ async def test_entity_availability(hass, monkeypatch): config[CONF_RECONNECT_INTERVAL] = 60 # Create platform and entities - _, _, _, disconnect_callback = await mock_rflink( + event_callback, _, _, disconnect_callback = await mock_rflink( hass, config, DOMAIN, monkeypatch, failures=failures ) - # Entities are available by default + # Entities are unknown by default assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN + # test binary_sensor status change + event_callback({"id": "test", "command": "on"}) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state == STATE_ON + # Mock a disconnect of the Rflink device disconnect_callback() @@ -113,8 +119,8 @@ async def test_entity_availability(hass, monkeypatch): # Wait for dispatch events to propagate await hass.async_block_till_done() - # Entities should be available again - assert hass.states.get("binary_sensor.test").state == STATE_UNKNOWN + # Entities should restore its status + assert hass.states.get("binary_sensor.test").state == STATE_ON async def test_off_delay(hass, legacy_patchable_time, monkeypatch): @@ -129,12 +135,12 @@ async def test_off_delay(hass, legacy_patchable_time, monkeypatch): on_event = {"id": "test2", "command": "on"} - @ha.callback - def callback(event): + @callback + def listener(event): """Verify event got called.""" events.append(event) - hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + hass.bus.async_listen(EVENT_STATE_CHANGED, listener) now = dt_util.utcnow() # fake time and turn on sensor @@ -178,3 +184,24 @@ def callback(event): state = hass.states.get("binary_sensor.test2") assert state.state == STATE_OFF assert len(events) == 3 + + +async def test_restore_state(hass, monkeypatch): + """Ensure states are restored on startup.""" + mock_restore_cache( + hass, (State(f"{DOMAIN}.test", STATE_ON), State(f"{DOMAIN}.test2", STATE_ON)) + ) + + hass.state = CoreState.starting + + # setup mocking rflink module + _, _, _, _ = await mock_rflink(hass, CONFIG, DOMAIN, monkeypatch) + + state = hass.states.get(f"{DOMAIN}.test") + assert state + assert state.state == STATE_ON + + # off_delay config must restore to off + state = hass.states.get(f"{DOMAIN}.test2") + assert state + assert state.state == STATE_OFF diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 06e37545d252b..70de0be5937c0 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -1,4 +1,6 @@ """Common test tools.""" +from __future__ import annotations + from datetime import timedelta from unittest.mock import patch @@ -24,6 +26,23 @@ def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None): } +async def setup_rfx_test_cfg( + hass, device="abcd", automatic_add=False, devices: dict[str, dict] | None = None +): + """Construct a rfxtrx config entry.""" + entry_data = create_rfx_test_cfg( + device=device, automatic_add=automatic_add, devices=devices + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + mock_entry.supports_remove_device = True + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.async_start() + return mock_entry + + @pytest.fixture(autouse=True, name="rfxtrx") async def rfxtrx_fixture(hass): """Fixture that cleans up threads from integration.""" @@ -50,14 +69,7 @@ async def _signal_event(packet_id): @pytest.fixture(name="rfxtrx_automatic") async def rfxtrx_automatic_fixture(hass, rfxtrx): """Fixture that starts up with automatic additions.""" - entry_data = create_rfx_test_cfg(automatic_add=True, devices={}) - mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - - mock_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - await hass.async_start() + await setup_rfx_test_cfg(hass, automatic_add=True, devices={}) yield rfxtrx diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 96368d166d773..175b455da6b78 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -38,7 +38,7 @@ async def test_one(hass, rfxtrx): async def test_one_pt2262(hass, rfxtrx): - """Test with 1 sensor.""" + """Test with 1 PT2262 sensor.""" entry_data = create_rfx_test_cfg( devices={ "0913000022670e013970": { diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index cd87f02c89399..bee9ea4880abd 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -408,88 +408,6 @@ async def test_options_add_duplicate_device(hass): assert result["errors"]["event_code"] == "already_configured_device" -async def test_options_add_remove_device(hass): - """Test we can add a device.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": None, - "port": None, - "device": "/dev/tty123", - "automatic_add": False, - "devices": {}, - }, - unique_id=DOMAIN, - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "prompt_options" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "automatic_add": True, - "event_code": "0b1100cd0213c7f230010f71", - }, - ) - - assert result["type"] == "form" - assert result["step_id"] == "set_device_options" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"signal_repetitions": 5, "off_delay": "4"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - await hass.async_block_till_done() - - assert entry.data["automatic_add"] - - assert entry.data["devices"]["0b1100cd0213c7f230010f71"] - assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["signal_repetitions"] == 5 - assert entry.data["devices"]["0b1100cd0213c7f230010f71"]["off_delay"] == 4 - - state = hass.states.get("binary_sensor.ac_213c7f2_48") - assert state - assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "AC 213c7f2:48" - - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - - assert device_entries[0].id - - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "prompt_options" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "automatic_add": False, - "remove_device": [device_entries[0].id], - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - await hass.async_block_till_done() - - assert not entry.data["automatic_add"] - - assert "0b1100cd0213c7f230010f71" not in entry.data["devices"] - - state = hass.states.get("binary_sensor.ac_213c7f2_48") - assert not state - - async def test_options_replace_sensor_device(hass): """Test we can replace a sensor device.""" @@ -758,76 +676,6 @@ async def test_options_replace_control_device(hass): assert not state -async def test_options_remove_multiple_devices(hass): - """Test we can add a device.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "host": None, - "port": None, - "device": "/dev/tty123", - "automatic_add": False, - "devices": { - "0b1100cd0213c7f230010f71": {"device_id": ["11", "0", "213c7f2:48"]}, - "0b1100100118cdea02010f70": {"device_id": ["11", "0", "118cdea:2"]}, - "0b1100101118cdea02010f70": {"device_id": ["11", "0", "1118cdea:2"]}, - }, - }, - unique_id=DOMAIN, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.ac_213c7f2_48") - assert state - state = hass.states.get("binary_sensor.ac_118cdea_2") - assert state - state = hass.states.get("binary_sensor.ac_1118cdea_2") - assert state - - device_registry = dr.async_get(hass) - device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - - assert len(device_entries) == 3 - - def match_device_id(entry): - device_id = next(iter(entry.identifiers))[1:] - if device_id == ("11", "0", "213c7f2:48"): - return True - if device_id == ("11", "0", "118cdea:2"): - return True - return False - - remove_devices = [elem.id for elem in device_entries if match_device_id(elem)] - - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == "form" - assert result["step_id"] == "prompt_options" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "automatic_add": False, - "remove_device": remove_devices, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.ac_213c7f2_48") - assert not state - state = hass.states.get("binary_sensor.ac_118cdea_2") - assert not state - state = hass.states.get("binary_sensor.ac_1118cdea_2") - assert state - - async def test_options_add_and_configure_device(hass): """Test we can add a device.""" diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 4ad5f9a342af6..d5562fa51496c 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,19 +1,20 @@ """The tests for the Rfxtrx component.""" +from __future__ import annotations from unittest.mock import call -from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.rfxtrx.conftest import create_rfx_test_cfg +from tests.components.rfxtrx.conftest import setup_rfx_test_cfg async def test_fire_event(hass, rfxtrx): """Test fire event.""" - entry_data = create_rfx_test_cfg( + await setup_rfx_test_cfg( + hass, device="/dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0", automatic_add=True, devices={ @@ -21,13 +22,6 @@ async def test_fire_event(hass, rfxtrx): "0716000100900970": {}, }, ) - mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - - mock_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - await hass.async_start() device_registry: dr.DeviceRegistry = dr.async_get(hass) @@ -78,13 +72,7 @@ def record_event(event): async def test_send(hass, rfxtrx): """Test configuration.""" - entry_data = create_rfx_test_cfg(device="/dev/null", devices={}) - mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - - mock_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + await setup_rfx_test_cfg(hass, device="/dev/null", devices={}) await hass.services.async_call( "rfxtrx", "send", {"event": "0a520802060101ff0f0269"}, blocking=True @@ -93,3 +81,40 @@ async def test_send(hass, rfxtrx): assert rfxtrx.transport.send.mock_calls == [ call(bytearray(b"\x0a\x52\x08\x02\x06\x01\x01\xff\x0f\x02\x69")) ] + + +async def test_ws_device_remove(hass, hass_ws_client): + """Test removing a device through device registry.""" + assert await async_setup_component(hass, "config", {}) + + device_id = ["11", "0", "213c7f2:16"] + mock_entry = await setup_rfx_test_cfg( + hass, + devices={ + "0b1100cd0213c7f210010f51": {"fire_event": True, "device_id": device_id}, + }, + ) + + device_reg = dr.async_get(hass) + + device_entry = device_reg.async_get_device(identifiers={("rfxtrx", *device_id)}) + assert device_entry + + # Ask to remove existing device + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mock_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + + # Verify device entry is removed + assert device_reg.async_get_device(identifiers={("rfxtrx", *device_id)}) is None + + # Verify that the config entry has removed the device + assert mock_entry.data["devices"] == {} diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 6c22ee02920d0..a4560934ee1de 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -52,6 +52,50 @@ async def test_one_switch(hass, rfxtrx): ] +async def test_one_pt2262_switch(hass, rfxtrx): + """Test with 1 PT2262 switch.""" + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "signal_repetitions": 1, + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.pt2262_22670e") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.pt2262_22670e"}, blocking=True + ) + + state = hass.states.get("switch.pt2262_22670e") + assert state.state == "on" + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.pt2262_22670e"}, blocking=True + ) + + state = hass.states.get("switch.pt2262_22670e") + assert state.state == "off" + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x09\x13\x00\x00\x22\x67\x0e\x01\x39\x00")), + call(bytearray(b"\x09\x13\x00\x00\x22\x67\x0f\x01\x39\x00")), + ] + + @pytest.mark.parametrize("state", ["on", "off"]) async def test_state_restore(hass, rfxtrx, state): """State restoration.""" @@ -182,6 +226,47 @@ async def test_switch_events(hass, rfxtrx): assert hass.states.get("switch.ac_213c7f2_16").state == "off" +async def test_pt2262_switch_events(hass, rfxtrx): + """Test with 1 PT2262 switch.""" + entry_data = create_rfx_test_cfg( + devices={ + "0913000022670e013970": { + "signal_repetitions": 1, + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("switch.pt2262_22670e") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + # "Command: 0xE" + await rfxtrx.signal("0913000022670e013970") + assert hass.states.get("switch.pt2262_22670e").state == "on" + + # "Command: 0x0" + await rfxtrx.signal("09130000226700013970") + assert hass.states.get("switch.pt2262_22670e").state == "on" + + # "Command: 0x7" + await rfxtrx.signal("09130000226707013d70") + assert hass.states.get("switch.pt2262_22670e").state == "off" + + # "Command: 0x1" + await rfxtrx.signal("09130000226701013d70") + assert hass.states.get("switch.pt2262_22670e").state == "off" + + async def test_discover_switch(hass, rfxtrx_automatic): """Test with discovery of switches.""" rfxtrx = rfxtrx_automatic diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index c0e044a358947..2ae0b308f9acc 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -24,6 +24,7 @@ MOCK_HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=HOMEKIT_HOST, + addresses=[HOMEKIT_HOST], hostname="mock_hostname", name="onn._hap._tcp.local.", port=None, diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 16261e07a8910..677a10c697cb2 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -38,38 +38,44 @@ def mock_setup_entry() -> Generator[None, None, None]: @pytest.fixture -def mock_roku_config_flow( +async def mock_device( request: pytest.FixtureRequest, -) -> Generator[None, MagicMock, None]: - """Return a mocked Roku client.""" +) -> RokuDevice: + """Return the mocked roku device.""" fixture: str = "roku/roku3.json" if hasattr(request, "param") and request.param: fixture = request.param - device = RokuDevice(json.loads(load_fixture(fixture))) + return RokuDevice(json.loads(load_fixture(fixture))) + + +@pytest.fixture +def mock_roku_config_flow( + mock_device: RokuDevice, +) -> Generator[None, MagicMock, None]: + """Return a mocked Roku client.""" + with patch( "homeassistant.components.roku.config_flow.Roku", autospec=True ) as roku_mock: client = roku_mock.return_value client.app_icon_url.side_effect = app_icon_url - client.update.return_value = device + client.update.return_value = mock_device yield client @pytest.fixture -def mock_roku(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_roku( + request: pytest.FixtureRequest, mock_device: RokuDevice +) -> Generator[None, MagicMock, None]: """Return a mocked Roku client.""" - fixture: str = "roku/roku3.json" - if hasattr(request, "param") and request.param: - fixture = request.param - device = RokuDevice(json.loads(load_fixture(fixture))) with patch( "homeassistant.components.roku.coordinator.Roku", autospec=True ) as roku_mock: client = roku_mock.return_value client.app_icon_url.side_effect = app_icon_url - client.update.return_value = device + client.update.return_value = mock_device yield client diff --git a/tests/components/roku/fixtures/rokutv-7820x.json b/tests/components/roku/fixtures/rokutv-7820x.json index 42181b087458b..17c29ace2de1d 100644 --- a/tests/components/roku/fixtures/rokutv-7820x.json +++ b/tests/components/roku/fixtures/rokutv-7820x.json @@ -167,6 +167,18 @@ "name": "QVC", "type": "air-digital", "user-hidden": "false" + }, + { + "number": "14.3", + "name": "getTV", + "type": "air-digital", + "user-hidden": "false" + }, + { + "number": "99.1", + "name": "", + "type": "air-digital", + "user-hidden": "false" } ], "media": { diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index d551a548c4c10..24f92b0b11b61 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock import pytest +from rokuecp import Device as RokuDevice from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.roku.const import DOMAIN @@ -82,10 +83,11 @@ async def test_roku_binary_sensors( assert device_entry.suggested_area is None -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_binary_sensors( hass: HomeAssistant, init_integration: MockConfigEntry, + mock_device: RokuDevice, mock_roku: MagicMock, ) -> None: """Test the Roku binary sensors.""" diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 99d0d1bb2c015..f5a3d270f7087 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -158,9 +158,7 @@ async def test_homekit_unknown_error( assert result["reason"] == "unknown" -@pytest.mark.parametrize( - "mock_roku_config_flow", ["roku/rokutv-7820x.json"], indirect=True -) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_homekit_discovery( hass: HomeAssistant, mock_roku_config_flow: MagicMock, diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index a039b313702ac..050814e381710 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -27,7 +27,9 @@ MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, + MEDIA_TYPE_VIDEO, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_BROWSE_MEDIA, @@ -115,7 +117,7 @@ async def test_setup(hass: HomeAssistant, init_integration: MockConfigEntry) -> assert device_entry.suggested_area is None -@pytest.mark.parametrize("mock_roku", ["roku/roku3-idle.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/roku3-idle.json"], indirect=True) async def test_idle_setup( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -127,7 +129,7 @@ async def test_idle_setup( assert state.state == STATE_STANDBY -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_setup( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -215,7 +217,7 @@ async def test_supported_features( ) -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_supported_features( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -254,7 +256,7 @@ async def test_attributes( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku" -@pytest.mark.parametrize("mock_roku", ["roku/roku3-app.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/roku3-app.json"], indirect=True) async def test_attributes_app( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -271,7 +273,9 @@ async def test_attributes_app( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Netflix" -@pytest.mark.parametrize("mock_roku", ["roku/roku3-media-playing.json"], indirect=True) +@pytest.mark.parametrize( + "mock_device", ["roku/roku3-media-playing.json"], indirect=True +) async def test_attributes_app_media_playing( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -290,7 +294,7 @@ async def test_attributes_app_media_playing( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Pluto TV - It's Free TV" -@pytest.mark.parametrize("mock_roku", ["roku/roku3-media-paused.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/roku3-media-paused.json"], indirect=True) async def test_attributes_app_media_paused( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -309,7 +313,7 @@ async def test_attributes_app_media_paused( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Pluto TV - It's Free TV" -@pytest.mark.parametrize("mock_roku", ["roku/roku3-screensaver.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/roku3-screensaver.json"], indirect=True) async def test_attributes_screensaver( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -326,7 +330,7 @@ async def test_attributes_screensaver( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku" -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_attributes( hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: @@ -457,72 +461,181 @@ async def test_services( "291097", { "contentID": "8e06a8b7-d667-4e31-939d-f40a6dd78a88", - "MediaType": "movie", + "mediaType": "movie", }, ) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: "Netflix"}, + blocking=True, + ) + + assert mock_roku.launch.call_count == 3 + mock_roku.launch.assert_called_with("12") + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: 12}, + blocking=True, + ) + + assert mock_roku.launch.call_count == 4 + mock_roku.launch.assert_called_with("12") + + +async def test_services_play_media( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_roku: MagicMock, +) -> None: + """Test the media player services related to playing media.""" await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: MAIN_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, - ATTR_MEDIA_CONTENT_ID: "https://awesome.tld/media.mp4", + ATTR_MEDIA_CONTENT_TYPE: "blah", + ATTR_MEDIA_CONTENT_ID: "https://localhost/media.m4a", ATTR_MEDIA_EXTRA: { - ATTR_NAME: "Sent from HA", - ATTR_FORMAT: "mp4", + ATTR_NAME: "Test", }, }, blocking=True, ) - assert mock_roku.play_on_roku.call_count == 1 - mock_roku.play_on_roku.assert_called_with( - "https://awesome.tld/media.mp4", + assert mock_roku.play_on_roku.call_count == 0 + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, { - "videoName": "Sent from HA", - "videoFormat": "mp4", + ATTR_ENTITY_ID: MAIN_ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "https://localhost/media.m4a", + ATTR_MEDIA_EXTRA: {ATTR_FORMAT: "blah"}, }, + blocking=True, ) + assert mock_roku.play_on_roku.call_count == 0 + + +@pytest.mark.parametrize( + "content_type, content_id, resolved_name, resolved_format", + [ + (MEDIA_TYPE_URL, "http://localhost/media.m4a", "media.m4a", "m4a"), + (MEDIA_TYPE_MUSIC, "http://localhost/media.m4a", "media.m4a", "m4a"), + (MEDIA_TYPE_MUSIC, "http://localhost/media.mka", "media.mka", "mka"), + ( + MEDIA_TYPE_MUSIC, + "http://localhost/api/tts_proxy/generated.mp3", + "Text to Speech", + "mp3", + ), + ], +) +async def test_services_play_media_audio( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_roku: MagicMock, + content_type: str, + content_id: str, + resolved_name: str, + resolved_format: str, +) -> None: + """Test the media player services related to playing media.""" await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: MAIN_ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[HLS_PROVIDER], - ATTR_MEDIA_CONTENT_ID: "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + ATTR_MEDIA_CONTENT_TYPE: content_type, + ATTR_MEDIA_CONTENT_ID: content_id, }, blocking=True, ) - - assert mock_roku.play_on_roku.call_count == 2 - mock_roku.play_on_roku.assert_called_with( - "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + mock_roku.play_on_roku.assert_called_once_with( + content_id, { - "MediaType": "hls", + "t": "a", + "songName": resolved_name, + "songFormat": resolved_format, + "artistName": "Home Assistant", }, ) + +@pytest.mark.parametrize( + "content_type, content_id, resolved_name, resolved_format", + [ + (MEDIA_TYPE_URL, "http://localhost/media.mp4", "media.mp4", "mp4"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.m4v", "media.m4v", "mp4"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.mov", "media.mov", "mp4"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.mkv", "media.mkv", "mkv"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.mks", "media.mks", "mks"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.m3u8", "media.m3u8", "hls"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.dash", "media.dash", "dash"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.mpd", "media.mpd", "dash"), + (MEDIA_TYPE_VIDEO, "http://localhost/media.ism/manifest", "media.ism", "ism"), + ], +) +async def test_services_play_media_video( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_roku: MagicMock, + content_type: str, + content_id: str, + resolved_name: str, + resolved_format: str, +) -> None: + """Test the media player services related to playing media.""" await hass.services.async_call( MP_DOMAIN, - SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: "Netflix"}, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: MAIN_ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: content_type, + ATTR_MEDIA_CONTENT_ID: content_id, + }, blocking=True, ) + mock_roku.play_on_roku.assert_called_once_with( + content_id, + { + "videoName": resolved_name, + "videoFormat": resolved_format, + }, + ) - assert mock_roku.launch.call_count == 3 - mock_roku.launch.assert_called_with("12") +async def test_services_camera_play_stream( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_roku: MagicMock, +) -> None: + """Test the media player services related to playing camera stream.""" await hass.services.async_call( MP_DOMAIN, - SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: 12}, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: MAIN_ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[HLS_PROVIDER], + ATTR_MEDIA_CONTENT_ID: "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + }, blocking=True, ) - assert mock_roku.launch.call_count == 4 - mock_roku.launch.assert_called_with("12") + assert mock_roku.play_on_roku.call_count == 1 + mock_roku.play_on_roku.assert_called_with( + "https://awesome.tld/api/hls/api_token/master_playlist.m3u8", + { + "videoName": "Camera Stream", + "videoFormat": "hls", + }, + ) async def test_services_play_media_local_source( @@ -554,10 +667,14 @@ async def test_services_play_media_local_source( assert mock_roku.play_on_roku.call_count == 1 assert mock_roku.play_on_roku.call_args call_args = mock_roku.play_on_roku.call_args.args - assert "/media/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] + assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0] + assert call_args[1] == { + "videoFormat": "mp4", + "videoName": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + } -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_services( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -642,7 +759,10 @@ async def test_media_browse( assert msg["result"]["children"][0]["title"] == "Roku Channel Store" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][0]["thumbnail"] + assert ( + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][0]["can_play"] # test invalid media type @@ -836,7 +956,7 @@ async def test_media_browse_local_source( ) -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_tv_media_browse( hass, init_integration, @@ -899,14 +1019,18 @@ async def test_tv_media_browse( assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" assert ( - "/browse_media/app/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"] + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/tvinput.hdmi2" ) assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][3]["title"] == "Roku Channel Store" assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][3]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][3]["thumbnail"] + assert ( + msg["result"]["children"][3]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][3]["can_play"] # test channels @@ -933,10 +1057,10 @@ async def test_tv_media_browse( assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] - assert len(msg["result"]["children"]) == 2 + assert len(msg["result"]["children"]) == 4 assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL - assert msg["result"]["children"][0]["title"] == "WhatsOn" + assert msg["result"]["children"][0]["title"] == "WhatsOn (1.1)" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL assert msg["result"]["children"][0]["media_content_id"] == "1.1" assert msg["result"]["children"][0]["can_play"] diff --git a/tests/components/roku/test_select.py b/tests/components/roku/test_select.py new file mode 100644 index 0000000000000..e82a13c851125 --- /dev/null +++ b/tests/components/roku/test_select.py @@ -0,0 +1,241 @@ +"""Tests for the Roku select platform.""" +from unittest.mock import MagicMock + +import pytest +from rokuecp import Application, Device as RokuDevice, RokuError + +from homeassistant.components.roku.const import DOMAIN +from homeassistant.components.roku.coordinator import SCAN_INTERVAL +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, SERVICE_SELECT_OPTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_application_state( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_device: RokuDevice, + mock_roku: MagicMock, +) -> None: + """Test the creation and values of the Roku selects.""" + entity_registry = er.async_get(hass) + + entity_registry.async_get_or_create( + SELECT_DOMAIN, + DOMAIN, + "1GU48T017973_application", + suggested_object_id="my_roku_3_application", + disabled_by=None, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("select.my_roku_3_application") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:application" + assert state.attributes.get(ATTR_OPTIONS) == [ + "Home", + "Amazon Video on Demand", + "Free FrameChannel Service", + "MLB.TV" + "\u00AE", + "Mediafly", + "Netflix", + "Pandora", + "Pluto TV - It's Free TV", + "Roku Channel Store", + ] + assert state.state == "Home" + + entry = entity_registry.async_get("select.my_roku_3_application") + assert entry + assert entry.unique_id == "1GU48T017973_application" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_roku_3_application", + ATTR_OPTION: "Netflix", + }, + blocking=True, + ) + + assert mock_roku.launch.call_count == 1 + mock_roku.launch.assert_called_with("12") + mock_device.app = mock_device.apps[1] + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.my_roku_3_application") + assert state + + assert state.state == "Netflix" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_roku_3_application", + ATTR_OPTION: "Home", + }, + blocking=True, + ) + + assert mock_roku.remote.call_count == 1 + mock_roku.remote.assert_called_with("home") + mock_device.app = Application( + app_id=None, name="Roku", version=None, screensaver=None + ) + async_fire_time_changed(hass, dt_util.utcnow() + (SCAN_INTERVAL * 2)) + await hass.async_block_till_done() + + state = hass.states.get("select.my_roku_3_application") + assert state + assert state.state == "Home" + + +async def test_application_select_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roku: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the Roku selects.""" + entity_registry = er.async_get(hass) + + entity_registry.async_get_or_create( + SELECT_DOMAIN, + DOMAIN, + "1GU48T017973_application", + suggested_object_id="my_roku_3_application", + disabled_by=None, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_roku.launch.side_effect = RokuError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_roku_3_application", + ATTR_OPTION: "Netflix", + }, + blocking=True, + ) + + state = hass.states.get("select.my_roku_3_application") + assert state + assert state.state == "Home" + assert "Invalid response from API" in caplog.text + assert mock_roku.launch.call_count == 1 + mock_roku.launch.assert_called_with("12") + + +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) +async def test_channel_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_device: RokuDevice, + mock_roku: MagicMock, +) -> None: + """Test the creation and values of the Roku selects.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("select.58_onn_roku_tv_channel") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:television" + assert state.attributes.get(ATTR_OPTIONS) == [ + "99.1", + "QVC (1.3)", + "WhatsOn (1.1)", + "getTV (14.3)", + ] + assert state.state == "getTV (14.3)" + + entry = entity_registry.async_get("select.58_onn_roku_tv_channel") + assert entry + assert entry.unique_id == "YN00H5555555_channel" + + # channel name + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.58_onn_roku_tv_channel", + ATTR_OPTION: "WhatsOn (1.1)", + }, + blocking=True, + ) + + assert mock_roku.tune.call_count == 1 + mock_roku.tune.assert_called_with("1.1") + mock_device.channel = mock_device.channels[0] + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.58_onn_roku_tv_channel") + assert state + assert state.state == "WhatsOn (1.1)" + + # channel number + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.58_onn_roku_tv_channel", + ATTR_OPTION: "99.1", + }, + blocking=True, + ) + + assert mock_roku.tune.call_count == 2 + mock_roku.tune.assert_called_with("99.1") + mock_device.channel = mock_device.channels[3] + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.58_onn_roku_tv_channel") + assert state + assert state.state == "99.1" + + +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) +async def test_channel_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_roku: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the Roku selects.""" + mock_roku.tune.side_effect = RokuError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.58_onn_roku_tv_channel", + ATTR_OPTION: "99.1", + }, + blocking=True, + ) + + state = hass.states.get("select.58_onn_roku_tv_channel") + assert state + assert state.state == "getTV (14.3)" + assert "Invalid response from API" in caplog.text + assert mock_roku.tune.call_count == 1 + mock_roku.tune.assert_called_with("99.1") diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index 6ca27635d3022..983455255faf5 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -65,7 +65,7 @@ async def test_roku_sensors( assert device_entry.suggested_area is None -@pytest.mark.parametrize("mock_roku", ["roku/rokutv-7820x.json"], indirect=True) +@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True) async def test_rokutv_sensors( hass: HomeAssistant, init_integration: MockConfigEntry, diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index bdc894c334335..ffdb4e5ba9af8 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -46,6 +46,9 @@ async def test_get_rss_feed(mock_http_client, hass): text = await resp.text() xml = ElementTree.fromstring(text) - assert xml[0].text == "feed title is a_state_1" - assert xml[1][0].text == "item title is a_state_2" - assert xml[1][1].text == "desc a_state_3" + feed_title = xml.find("./channel/title").text + item_title = xml.find("./channel/item/title").text + item_description = xml.find("./channel/item/description").text + assert feed_title == "feed title is a_state_1" + assert item_title == "item title is a_state_2" + assert item_description == "desc a_state_3" diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 14f33524f5286..e1cb4f8608216 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Samsung TV.""" +from datetime import datetime from unittest.mock import Mock, patch import pytest @@ -7,6 +8,8 @@ import homeassistant.util.dt as dt_util +from .const import SAMPLE_APP_LIST + @pytest.fixture(autouse=True) def fake_host_fixture() -> None: @@ -19,7 +22,7 @@ def fake_host_fixture() -> None: @pytest.fixture(name="remote") -def remote_fixture(): +def remote_fixture() -> Mock: """Patch the samsungctl Remote.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote_class: remote = Mock(Remote) @@ -30,7 +33,7 @@ def remote_fixture(): @pytest.fixture(name="remotews") -def remotews_fixture(): +def remotews_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" @@ -48,13 +51,14 @@ def remotews_fixture(): "networkType": "wireless", }, } + remotews.app_list.return_value = SAMPLE_APP_LIST remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews yield remotews @pytest.fixture(name="remotews_no_device_info") -def remotews_no_device_info_fixture(): +def remotews_no_device_info_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" @@ -69,7 +73,7 @@ def remotews_no_device_info_fixture(): @pytest.fixture(name="remotews_soundbar") -def remotews_soundbar_fixture(): +def remotews_soundbar_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" @@ -93,7 +97,7 @@ def remotews_soundbar_fixture(): @pytest.fixture(name="delay") -def delay_fixture(): +def delay_fixture() -> Mock: """Patch the delay script function.""" with patch( "homeassistant.components.samsungtv.media_player.Script.async_run" @@ -102,13 +106,13 @@ def delay_fixture(): @pytest.fixture -def mock_now(): +def mock_now() -> datetime: """Fixture for dtutil.now.""" return dt_util.utcnow() @pytest.fixture(name="no_mac_address") -def mac_address_fixture(): +def mac_address_fixture() -> Mock: """Patch getmac.get_mac_address.""" with patch("getmac.get_mac_address", return_value=None) as mac: yield mac diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py new file mode 100644 index 0000000000000..d56e540e64b32 --- /dev/null +++ b/tests/components/samsungtv/const.py @@ -0,0 +1,24 @@ +"""Constants for the samsungtv tests.""" +SAMPLE_APP_LIST = [ + { + "appId": "111299001912", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/111299001912/250x250.png", + "is_lock": 0, + "name": "YouTube", + }, + { + "appId": "3201608010191", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201608010191/250x250.png", + "is_lock": 0, + "name": "Deezer", + }, + { + "appId": "3201606009684", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201606009684/250x250.png", + "is_lock": 0, + "name": "Spotify - Music and Podcasts", + }, +] diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4eedb7c21076d..eb8ca745819be 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -2,6 +2,7 @@ import socket from unittest.mock import Mock, call, patch +import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws import SamsungTVWS from samsungtvws.exceptions import ConnectionFailure, HttpApiError @@ -43,6 +44,8 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .const import SAMPLE_APP_LIST + from tests.common import MockConfigEntry RESULT_ALREADY_CONFIGURED = "already_configured" @@ -101,6 +104,7 @@ EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host="fake_host", + addresses=["fake_host"], hostname="mock_hostname", name="mock_name", port=1234, @@ -181,7 +185,8 @@ } -async def test_user_legacy(hass: HomeAssistant, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_user_legacy(hass: HomeAssistant) -> None: """Test starting a flow by user.""" # show form result = await hass.config_entries.flow.async_init( @@ -205,7 +210,8 @@ async def test_user_legacy(hass: HomeAssistant, remote: Mock): assert result["result"].unique_id is None -async def test_user_websocket(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_user_websocket(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom") @@ -232,7 +238,8 @@ async def test_user_websocket(hass: HomeAssistant, remotews: Mock): assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_user_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow by user with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -246,7 +253,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): assert result["reason"] == RESULT_AUTH_MISSING -async def test_user_legacy_not_supported(hass: HomeAssistant): +async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -260,13 +267,13 @@ async def test_user_legacy_not_supported(hass: HomeAssistant): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_websocket_not_supported(hass: HomeAssistant): +async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=WebSocketProtocolException("Boom"), ): # websocket device not supported @@ -277,13 +284,13 @@ async def test_user_websocket_not_supported(hass: HomeAssistant): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_not_successful(hass: HomeAssistant): +async def test_user_not_successful(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=OSError("Boom"), ): result = await hass.config_entries.flow.async_init( @@ -293,13 +300,13 @@ async def test_user_not_successful(hass: HomeAssistant): assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_not_successful_2(hass: HomeAssistant): +async def test_user_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=ConnectionFailure("Boom"), ): result = await hass.config_entries.flow.async_init( @@ -309,12 +316,13 @@ async def test_user_not_successful_2(hass: HomeAssistant): assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): +@pytest.mark.usefixtures("remote") +async def test_ssdp(hass: HomeAssistant, no_mac_address: Mock) -> None: """Test starting a flow from discovery.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): # confirm to add the entry @@ -337,12 +345,13 @@ async def test_ssdp(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): +@pytest.mark.usefixtures("remote") +async def test_ssdp_noprefix(hass: HomeAssistant, no_mac_address: Mock) -> None: """Test starting a flow from discovery without prefixes.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO_2, ): # confirm to add the entry @@ -372,7 +381,8 @@ async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock, no_mac_address: assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" -async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -389,7 +399,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): # missing authentication with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", + "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.async_try_connect", return_value=RESULT_AUTH_MISSING, ): result = await hass.config_entries.flow.async_configure( @@ -399,9 +409,8 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remotews: Mock): assert result["reason"] == RESULT_AUTH_MISSING -async def test_ssdp_legacy_not_supported( - hass: HomeAssistant, remote: Mock, remotews: Mock -): +@pytest.mark.usefixtures("remote", "remotews") +async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: """Test starting a flow from discovery for not supported device.""" # confirm to add the entry @@ -412,7 +421,7 @@ async def test_ssdp_legacy_not_supported( assert result["step_id"] == "confirm" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.try_connect", + "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.async_try_connect", return_value=RESULT_NOT_SUPPORTED, ): # device not supported @@ -423,11 +432,10 @@ async def test_ssdp_legacy_not_supported( assert result["reason"] == RESULT_NOT_SUPPORTED +@pytest.mark.usefixtures("remote", "remotews") async def test_ssdp_websocket_success_populates_mac_address( hass: HomeAssistant, - remote: Mock, - remotews: Mock, -): +) -> None: """Test starting a flow from ssdp for a supported device populates the mac.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -448,14 +456,15 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_websocket_not_supported(hass: HomeAssistant): +async def test_ssdp_websocket_not_supported(hass: HomeAssistant) -> None: """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=WebSocketProtocolException("Boom"), + ) as remotews, patch.object( + remotews, "open", side_effect=WebSocketProtocolException("Boom") ): # device not supported result = await hass.config_entries.flow.async_init( @@ -465,7 +474,8 @@ async def test_ssdp_websocket_not_supported(hass: HomeAssistant): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_model_not_supported(hass: HomeAssistant, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" # confirm to add the entry @@ -478,16 +488,17 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant, remote: Mock): assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_not_successful(hass: HomeAssistant, no_mac_address: Mock): +@pytest.mark.usefixtures("no_mac_address") +async def test_ssdp_not_successful(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): @@ -506,16 +517,17 @@ async def test_ssdp_not_successful(hass: HomeAssistant, no_mac_address: Mock): assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_not_successful_2(hass: HomeAssistant, no_mac_address: Mock): +@pytest.mark.usefixtures("no_mac_address") +async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=ConnectionFailure("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): @@ -534,14 +546,15 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant, no_mac_address: Mock): assert result["reason"] == RESULT_CANNOT_CONNECT +@pytest.mark.usefixtures("remote") async def test_ssdp_already_in_progress( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock -): + hass: HomeAssistant, no_mac_address: Mock +) -> None: """Test starting a flow from discovery twice.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): @@ -560,14 +573,15 @@ async def test_ssdp_already_in_progress( assert result["reason"] == RESULT_ALREADY_IN_PROGRESS +@pytest.mark.usefixtures("remote") async def test_ssdp_already_configured( - hass: HomeAssistant, remote: Mock, no_mac_address: Mock -): + hass: HomeAssistant, no_mac_address: Mock +) -> None: """Test starting a flow from discovery when already configured.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): @@ -592,7 +606,8 @@ async def test_ssdp_already_configured( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_import_legacy(hass: HomeAssistant, remote: Mock, no_mac_address: Mock): +@pytest.mark.usefixtures("remote") +async def test_import_legacy(hass: HomeAssistant, no_mac_address: Mock) -> None: """Test importing from yaml with hostname.""" no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" @@ -615,12 +630,8 @@ async def test_import_legacy(hass: HomeAssistant, remote: Mock, no_mac_address: assert entries[0].data[CONF_PORT] == LEGACY_PORT -async def test_import_legacy_without_name( - hass: HomeAssistant, - remote: Mock, - remotews_no_device_info: Mock, - no_mac_address: Mock, -): +@pytest.mark.usefixtures("remote", "remotews_no_device_info", "no_mac_address") +async def test_import_legacy_without_name(hass: HomeAssistant) -> None: """Test importing from yaml without a name.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -640,7 +651,8 @@ async def test_import_legacy_without_name( assert entries[0].data[CONF_PORT] == LEGACY_PORT -async def test_import_websocket(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -658,7 +670,8 @@ async def test_import_websocket(hass: HomeAssistant, remotews: Mock): assert result["result"].unique_id is None -async def test_import_websocket_without_port(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_import_websocket_without_port(hass: HomeAssistant): """Test importing from yaml with hostname by no port.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -679,7 +692,8 @@ async def test_import_websocket_without_port(hass: HomeAssistant, remotews: Mock assert entries[0].data[CONF_PORT] == 8002 -async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_import_unknown_host(hass: HomeAssistant): """Test importing from yaml with hostname that does not resolve.""" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", @@ -695,7 +709,8 @@ async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock): assert result["reason"] == RESULT_UNKNOWN_HOST -async def test_dhcp(hass: HomeAssistant, remote: Mock, remotews: Mock): +@pytest.mark.usefixtures("remote", "remotews") +async def test_dhcp(hass: HomeAssistant) -> None: """Test starting a flow from dhcp.""" # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -721,7 +736,8 @@ async def test_dhcp(hass: HomeAssistant, remote: Mock, remotews: Mock): assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_zeroconf(hass: HomeAssistant, remote: Mock, remotews: Mock): +@pytest.mark.usefixtures("remote", "remotews") +async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -746,7 +762,8 @@ async def test_zeroconf(hass: HomeAssistant, remote: Mock, remotews: Mock): assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, remotews_soundbar: Mock): +@pytest.mark.usefixtures("remotews_soundbar") +async def test_zeroconf_ignores_soundbar(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where the device is actually a soundbar.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -758,9 +775,8 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, remotews_soundbar: assert result["reason"] == "not_supported" -async def test_zeroconf_no_device_info( - hass: HomeAssistant, remote: Mock, remotews_no_device_info: Mock -): +@pytest.mark.usefixtures("remote", "remotews_no_device_info") +async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where device_info returns None.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -772,7 +788,8 @@ async def test_zeroconf_no_device_info( assert result["reason"] == "not_supported" -async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf and dhcp.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -793,7 +810,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant, remotews: Mock): assert result2["reason"] == "already_in_progress" -async def test_autodetect_websocket(hass: HomeAssistant): +async def test_autodetect_websocket(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -802,6 +819,7 @@ async def test_autodetect_websocket(hass: HomeAssistant): remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) + remote.app_list.return_value = SAMPLE_APP_LIST remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -835,7 +853,7 @@ async def test_autodetect_websocket(hass: HomeAssistant): assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_websocket_no_mac(hass: HomeAssistant): +async def test_websocket_no_mac(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -848,6 +866,7 @@ async def test_websocket_no_mac(hass: HomeAssistant): remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) + remote.app_list.return_value = SAMPLE_APP_LIST remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -880,7 +899,7 @@ async def test_websocket_no_mac(hass: HomeAssistant): assert entries[0].data[CONF_MAC] == "gg:hh:ii:ll:mm:nn" -async def test_autodetect_auth_missing(hass: HomeAssistant): +async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -895,7 +914,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant): assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_not_supported(hass: HomeAssistant): +async def test_autodetect_not_supported(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -910,7 +929,8 @@ async def test_autodetect_not_supported(hass: HomeAssistant): assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_autodetect_legacy(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -922,14 +942,19 @@ async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): assert result["data"][CONF_PORT] == LEGACY_PORT -async def test_autodetect_none(hass: HomeAssistant): +async def test_autodetect_none(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" + mock_remotews = Mock() + mock_remotews.__enter__ = Mock(return_value=mock_remotews) + mock_remotews.__exit__ = Mock() + mock_remotews.open = Mock(side_effect=OSError("Boom")) + with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ) as remote, patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=OSError("Boom"), + return_value=mock_remotews, ) as remotews: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -947,7 +972,8 @@ async def test_autodetect_none(hass: HomeAssistant): ] -async def test_update_old_entry(hass: HomeAssistant, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_update_old_entry(hass: HomeAssistant) -> None: """Test update of old entry.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: remote().rest_device_info.return_value = { @@ -988,7 +1014,10 @@ async def test_update_old_entry(hass: HomeAssistant, remotews: Mock): assert entry2.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_update_missing_mac_unique_id_added_from_dhcp( + hass: HomeAssistant, +) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) @@ -1014,7 +1043,10 @@ async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_update_missing_mac_unique_id_added_from_zeroconf( + hass: HomeAssistant, +) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) @@ -1039,7 +1071,10 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews: assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_update_missing_mac_unique_id_added_from_ssdp(hass, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_update_missing_mac_unique_id_added_from_ssdp( + hass: HomeAssistant, +) -> None: """Test missing mac and unique id added via ssdp.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) @@ -1065,9 +1100,10 @@ async def test_update_missing_mac_unique_id_added_from_ssdp(hass, remotews: Mock assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" +@pytest.mark.usefixtures("remotews") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( - hass, remotews: Mock -): + hass: HomeAssistant, +) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1096,7 +1132,8 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: """Test missing mac added.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1127,7 +1164,10 @@ async def test_update_legacy_missing_mac_from_dhcp(hass, remote: Mock): assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( + hass: HomeAssistant, +) -> None: """Test missing mac added when there is no unique id.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1163,7 +1203,8 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id(hass, remote: Mo assert entry.unique_id is None -async def test_form_reauth_legacy(hass, remote: Mock): +@pytest.mark.usefixtures("remote") +async def test_form_reauth_legacy(hass: HomeAssistant) -> None: """Test reauthenticate legacy.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) entry.add_to_hass(hass) @@ -1184,7 +1225,8 @@ async def test_form_reauth_legacy(hass, remote: Mock): assert result2["reason"] == "reauth_successful" -async def test_form_reauth_websocket(hass, remotews: Mock): +@pytest.mark.usefixtures("remotews") +async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) @@ -1208,7 +1250,9 @@ async def test_form_reauth_websocket(hass, remotews: Mock): assert entry.state == config_entries.ConfigEntryState.LOADED -async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): +async def test_form_reauth_websocket_cannot_connect( + hass: HomeAssistant, remotews: Mock +) -> None: """Test reauthenticate websocket when we cannot connect on the first attempt.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) @@ -1220,10 +1264,7 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=ConnectionFailure, - ): + with patch.object(remotews, "open", side_effect=ConnectionFailure): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -1243,7 +1284,7 @@ async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): assert result3["reason"] == "reauth_successful" -async def test_form_reauth_websocket_not_supported(hass): +async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: """Test reauthenticate websocket when the device is not supported.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) entry.add_to_hass(hass) @@ -1256,7 +1297,7 @@ async def test_form_reauth_websocket_not_supported(hass): assert result["errors"] == {} with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=WebSocketException, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 990c25c8f3e8f..67bc012ced1aa 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -55,5 +55,15 @@ async def test_entry_diagnostics( "title": "Mock Title", "unique_id": "any", "version": 2, - } + }, + "device_info": { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "name": "[TV] Living Room", + "networkType": "wireless", + "type": "Samsung SmartTV", + "wifiMac": "aa:bb:cc:dd:ee:ff", + }, + }, } diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index e49b8fdc5ee97..6b6aa4292430d 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,5 +1,7 @@ """Tests for the Samsung TV Integration.""" -from unittest.mock import Mock, patch +from unittest.mock import patch + +import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( @@ -53,7 +55,8 @@ } -async def test_setup(hass: HomeAssistant, remotews: Mock, no_mac_address: Mock): +@pytest.mark.usefixtures("remotews", "no_mac_address") +async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() @@ -72,7 +75,7 @@ async def test_setup(hass: HomeAssistant, remotews: Mock, no_mac_address: Mock): ) -async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): +async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant) -> None: """Test import from yaml when the device is offline.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError @@ -80,7 +83,7 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", side_effect=OSError, ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.device_info", + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=None, ): await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -91,9 +94,8 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant): assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY -async def test_setup_from_yaml_without_port_device_online( - hass: HomeAssistant, remotews: Mock -): +@pytest.mark.usefixtures("remotews") +async def test_setup_from_yaml_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() @@ -103,7 +105,10 @@ async def test_setup_from_yaml_without_port_device_online( assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog): +@pytest.mark.usefixtures("remote") +async def test_setup_duplicate_config( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test duplicate setup of platform.""" duplicate = { SAMSUNGTV_DOMAIN: [ @@ -118,9 +123,8 @@ async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog) assert "duplicate host entries found" in caplog.text -async def test_setup_duplicate_entries( - hass: HomeAssistant, remote: Mock, remotews: Mock, no_mac_address: Mock -): +@pytest.mark.usefixtures("remote", "remotews", "no_mac_address") +async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 3f00475138e4b..55d68453a3814 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,6 +1,6 @@ """Tests for samsungtv component.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import logging from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, call, patch @@ -17,6 +17,7 @@ ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL, SERVICE_PLAY_MEDIA, @@ -56,9 +57,13 @@ STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .const import SAMPLE_APP_LIST + from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" @@ -131,30 +136,34 @@ def delay_fixture(): yield delay -async def setup_samsungtv(hass, config): +async def setup_samsungtv(hass: HomeAssistant, config: ConfigType) -> None: """Set up mock Samsung TV.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, config) await hass.async_block_till_done() -async def test_setup_with_turnon(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_setup_with_turnon(hass: HomeAssistant) -> None: """Test setup of platform.""" await setup_samsungtv(hass, MOCK_CONFIG) assert hass.states.get(ENTITY_ID) -async def test_setup_without_turnon(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_setup_without_turnon(hass: HomeAssistant) -> None: """Test setup of platform.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert hass.states.get(ENTITY_ID_NOTURNON) -async def test_setup_websocket(hass, remotews): +@pytest.mark.usefixtures("remotews") +async def test_setup_websocket(hass: HomeAssistant) -> None: """Test setup of platform.""" with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() + remote.app_list.return_value = SAMPLE_APP_LIST remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -184,7 +193,7 @@ async def test_setup_websocket(hass, remotews): assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_setup_websocket_2(hass, mock_now): +async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> None: """Test setup of platform from config entry.""" entity_id = f"{DOMAIN}.fake" @@ -203,6 +212,7 @@ async def test_setup_websocket_2(hass, mock_now): remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() + remote.app_list.return_value = SAMPLE_APP_LIST remote.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -231,7 +241,8 @@ async def test_setup_websocket_2(hass, mock_now): assert remote_class.call_args_list[0] == call(**MOCK_CALLS_WS) -async def test_update_on(hass, remote, mock_now): +@pytest.mark.usefixtures("remote") +async def test_update_on(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv on.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -244,7 +255,8 @@ async def test_update_on(hass, remote, mock_now): assert state.state == STATE_ON -async def test_update_off(hass, remote, mock_now): +@pytest.mark.usefixtures("remote") +async def test_update_off(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv off.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -262,7 +274,8 @@ async def test_update_off(hass, remote, mock_now): assert state.state == STATE_OFF -async def test_update_access_denied(hass, remote, mock_now): +@pytest.mark.usefixtures("remote") +async def test_update_access_denied(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv access denied exception.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -288,7 +301,9 @@ async def test_update_access_denied(hass, remote, mock_now): assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure(hass, remotews, mock_now): +async def test_update_connection_failure( + hass: HomeAssistant, mock_now: datetime, remotews: Mock +) -> None: """Testing update tv connection failure exception.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -296,10 +311,7 @@ async def test_update_connection_failure(hass, remotews, mock_now): ): await setup_samsungtv(hass, MOCK_CONFIGWS) - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=ConnectionFailure("Boom"), - ): + with patch.object(remotews, "open", side_effect=ConnectionFailure("Boom")): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -318,7 +330,10 @@ async def test_update_connection_failure(hass, remotews, mock_now): assert state.state == STATE_UNAVAILABLE -async def test_update_unhandled_response(hass, remote, mock_now): +@pytest.mark.usefixtures("remote") +async def test_update_unhandled_response( + hass: HomeAssistant, mock_now: datetime +) -> None: """Testing update tv unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -336,7 +351,10 @@ async def test_update_unhandled_response(hass, remote, mock_now): assert state.state == STATE_ON -async def test_connection_closed_during_update_can_recover(hass, remote, mock_now): +@pytest.mark.usefixtures("remote") +async def test_connection_closed_during_update_can_recover( + hass: HomeAssistant, mock_now: datetime +) -> None: """Testing update tv connection closed exception can recover.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -362,7 +380,7 @@ async def test_connection_closed_during_update_can_recover(hass, remote, mock_no assert state.state == STATE_ON -async def test_send_key(hass, remote): +async def test_send_key(hass: HomeAssistant, remote: Mock) -> None: """Test for send key.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -377,7 +395,7 @@ async def test_send_key(hass, remote): assert state.state == STATE_ON -async def test_send_key_broken_pipe(hass, remote): +async def test_send_key_broken_pipe(hass: HomeAssistant, remote: Mock) -> None: """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) remote.control = Mock(side_effect=BrokenPipeError("Boom")) @@ -388,7 +406,9 @@ async def test_send_key_broken_pipe(hass, remote): assert state.state == STATE_ON -async def test_send_key_connection_closed_retry_succeed(hass, remote): +async def test_send_key_connection_closed_retry_succeed( + hass: HomeAssistant, remote: Mock +) -> None: """Test retry on connection closed.""" await setup_samsungtv(hass, MOCK_CONFIG) remote.control = Mock( @@ -409,7 +429,7 @@ async def test_send_key_connection_closed_retry_succeed(hass, remote): assert state.state == STATE_ON -async def test_send_key_unhandled_response(hass, remote): +async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) @@ -420,7 +440,7 @@ async def test_send_key_unhandled_response(hass, remote): assert state.state == STATE_ON -async def test_send_key_websocketexception(hass, remote): +async def test_send_key_websocketexception(hass: HomeAssistant, remote: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) remote.control = Mock(side_effect=WebSocketException("Boom")) @@ -431,7 +451,7 @@ async def test_send_key_websocketexception(hass, remote): assert state.state == STATE_ON -async def test_send_key_os_error(hass, remote): +async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) remote.control = Mock(side_effect=OSError("Boom")) @@ -442,14 +462,16 @@ async def test_send_key_os_error(hass, remote): assert state.state == STATE_ON -async def test_name(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_name(hass: HomeAssistant) -> None: """Test for name property.""" await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_FRIENDLY_NAME] == "fake" -async def test_state_with_turnon(hass, remote, delay): +@pytest.mark.usefixtures("remote") +async def test_state_with_turnon(hass: HomeAssistant, delay: Mock) -> None: """Test for state property.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -466,7 +488,8 @@ async def test_state_with_turnon(hass, remote, delay): assert state.state == STATE_OFF -async def test_state_without_turnon(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_state_without_turnon(hass: HomeAssistant) -> None: """Test for state property.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert await hass.services.async_call( @@ -494,7 +517,8 @@ async def test_state_without_turnon(hass, remote): assert state.state == STATE_UNAVAILABLE -async def test_supported_features_with_turnon(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_supported_features_with_turnon(hass: HomeAssistant) -> None: """Test for supported_features property.""" await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) @@ -503,21 +527,23 @@ async def test_supported_features_with_turnon(hass, remote): ) -async def test_supported_features_without_turnon(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_supported_features_without_turnon(hass: HomeAssistant) -> None: """Test for supported_features property.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) state = hass.states.get(ENTITY_ID_NOTURNON) assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV -async def test_device_class(hass, remote): +@pytest.mark.usefixtures("remote") +async def test_device_class(hass: HomeAssistant) -> None: """Test for device_class property.""" await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_DEVICE_CLASS] is MediaPlayerDeviceClass.TV.value -async def test_turn_off_websocket(hass, remotews): +async def test_turn_off_websocket(hass: HomeAssistant, remotews: Mock) -> None: """Test for turn_off.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -532,7 +558,7 @@ async def test_turn_off_websocket(hass, remotews): assert remotews.send_key.call_args_list == [call("KEY_POWER")] -async def test_turn_off_legacy(hass, remote): +async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert await hass.services.async_call( @@ -543,7 +569,9 @@ async def test_turn_off_legacy(hass, remote): assert remote.control.call_args_list == [call("KEY_POWEROFF")] -async def test_turn_off_os_error(hass, remote, caplog): +async def test_turn_off_os_error( + hass: HomeAssistant, remote: Mock, caplog: pytest.LogCaptureFixture +) -> None: """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) await setup_samsungtv(hass, MOCK_CONFIG) @@ -554,7 +582,7 @@ async def test_turn_off_os_error(hass, remote, caplog): assert "Could not establish connection" in caplog.text -async def test_volume_up(hass, remote): +async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_up.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -567,7 +595,7 @@ async def test_volume_up(hass, remote): assert remote.close.call_args_list == [call()] -async def test_volume_down(hass, remote): +async def test_volume_down(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_down.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -580,7 +608,7 @@ async def test_volume_down(hass, remote): assert remote.close.call_args_list == [call()] -async def test_mute_volume(hass, remote): +async def test_mute_volume(hass: HomeAssistant, remote: Mock) -> None: """Test for mute_volume.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -596,7 +624,7 @@ async def test_mute_volume(hass, remote): assert remote.close.call_args_list == [call()] -async def test_media_play(hass, remote): +async def test_media_play(hass: HomeAssistant, remote: Mock) -> None: """Test for media_play.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -618,7 +646,7 @@ async def test_media_play(hass, remote): assert remote.close.call_args_list == [call(), call()] -async def test_media_pause(hass, remote): +async def test_media_pause(hass: HomeAssistant, remote: Mock) -> None: """Test for media_pause.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -640,7 +668,7 @@ async def test_media_pause(hass, remote): assert remote.close.call_args_list == [call(), call()] -async def test_media_next_track(hass, remote): +async def test_media_next_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_next_track.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -653,7 +681,7 @@ async def test_media_next_track(hass, remote): assert remote.close.call_args_list == [call()] -async def test_media_previous_track(hass, remote): +async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: """Test for media_previous_track.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -666,7 +694,8 @@ async def test_media_previous_track(hass, remote): assert remote.close.call_args_list == [call()] -async def test_turn_on_with_turnon(hass, remote, delay): +@pytest.mark.usefixtures("remote") +async def test_turn_on_with_turnon(hass: HomeAssistant, delay: Mock) -> None: """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -675,7 +704,8 @@ async def test_turn_on_with_turnon(hass, remote, delay): assert delay.call_count == 1 -async def test_turn_on_wol(hass, remotews): +@pytest.mark.usefixtures("remotews") +async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( domain=SAMSUNGTV_DOMAIN, @@ -695,7 +725,7 @@ async def test_turn_on_wol(hass, remotews): assert mock_send_magic_packet.called -async def test_turn_on_without_turnon(hass, remote): +async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert await hass.services.async_call( @@ -705,14 +735,14 @@ async def test_turn_on_without_turnon(hass, remote): assert remote.control.call_count == 0 -async def test_play_media(hass, remote): +async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: """Test for play_media.""" asyncio_sleep = asyncio.sleep sleeps = [] - async def sleep(duration, loop): + async def sleep(duration): sleeps.append(duration) - await asyncio_sleep(0, loop=loop) + await asyncio_sleep(0) await setup_samsungtv(hass, MOCK_CONFIG) with patch("asyncio.sleep", new=sleep): @@ -739,7 +769,7 @@ async def sleep(duration, loop): assert len(sleeps) == 3 -async def test_play_media_invalid_type(hass): +async def test_play_media_invalid_type(hass: HomeAssistant) -> None: """Test for play_media with invalid media type.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" @@ -761,7 +791,7 @@ async def test_play_media_invalid_type(hass): assert remote.call_count == 1 -async def test_play_media_channel_as_string(hass): +async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: """Test for play_media with invalid channel as string.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" @@ -783,7 +813,7 @@ async def test_play_media_channel_as_string(hass): assert remote.call_count == 1 -async def test_play_media_channel_as_non_positive(hass): +async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: """Test for play_media with invalid channel as non positive integer.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: await setup_samsungtv(hass, MOCK_CONFIG) @@ -804,7 +834,7 @@ async def test_play_media_channel_as_non_positive(hass): assert remote.call_count == 1 -async def test_select_source(hass, remote): +async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: """Test for select_source.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -820,7 +850,7 @@ async def test_select_source(hass, remote): assert remote.close.call_args_list == [call()] -async def test_select_source_invalid_source(hass): +async def test_select_source_invalid_source(hass: HomeAssistant) -> None: """Test for select_source with invalid source.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: await setup_samsungtv(hass, MOCK_CONFIG) @@ -835,3 +865,34 @@ async def test_select_source_invalid_source(hass): assert remote.control.call_count == 0 assert remote.close.call_count == 0 assert remote.call_count == 1 + + +async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: + """Test for play_media.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_APP, + ATTR_MEDIA_CONTENT_ID: "3201608010191", + }, + True, + ) + assert remotews.run_app.call_count == 1 + assert remotews.run_app.call_args_list == [call("3201608010191")] + + +async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: + """Test for select_source.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, + True, + ) + assert remotews.run_app.call_count == 1 + assert remotews.run_app.call_args_list == [call("3201608010191")] diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py new file mode 100644 index 0000000000000..0ba9266a79d96 --- /dev/null +++ b/tests/components/scrape/__init__.py @@ -0,0 +1,83 @@ +"""Tests for scrape component.""" +from __future__ import annotations + +from typing import Any + + +def return_config( + select, + name, + *, + attribute=None, + index=None, + template=None, + uom=None, + device_class=None, + state_class=None, + authentication=None, + username=None, + password=None, + headers=None, +) -> dict[str, dict[str, Any]]: + """Return config.""" + config = { + "platform": "scrape", + "resource": "https://www.home-assistant.io", + "select": select, + "name": name, + } + if attribute: + config["attribute"] = attribute + if index: + config["index"] = index + if template: + config["value_template"] = template + if uom: + config["unit_of_measurement"] = uom + if device_class: + config["device_class"] = device_class + if state_class: + config["state_class"] = state_class + if authentication: + config["authentication"] = authentication + config["username"] = username + config["password"] = password + if headers: + config["headers"] = headers + return config + + +class MockRestData: + """Mock RestData.""" + + def __init__( + self, + payload, + ): + """Init RestDataMock.""" + self.data: str | None = None + self.payload = payload + self.count = 0 + + async def async_update(self, data: bool | None = True) -> None: + """Update.""" + self.count += 1 + if self.payload == "test_scrape_sensor": + self.data = ( + "
" + "

Current Version: 2021.12.10

Released: January 17, 2022" + "
" + "" + ) + if self.payload == "test_scrape_uom_and_classes": + self.data = ( + "
" + "

Current Temperature: 22.1

" + "
" + ) + if self.payload == "test_scrape_sensor_authentication": + self.data = "
secret text
" + if self.payload == "test_scrape_sensor_no_data": + self.data = None + if self.count == 3: + self.data = None diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py new file mode 100644 index 0000000000000..aaf156208efd3 --- /dev/null +++ b/tests/components/scrape/test_sensor.py @@ -0,0 +1,206 @@ +"""The tests for the Scrape sensor platform.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor.const import CONF_STATE_CLASS +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.setup import async_setup_component + +from . import MockRestData, return_config + +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")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", 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_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] }}", + ) + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state.state == "2021.12.10" + + +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", + ) + } + + mocker = MockRestData("test_scrape_uom_and_classes") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.current_temp") + assert state.state == "22.1" + assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[CONF_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.MEASUREMENT + + +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", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", 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_no_data(hass: HomeAssistant) -> None: + """Test Scrape sensor fails on no data.""" + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state is None + + +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")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state + assert state.state == "Current Version: 2021.12.10" + + mocker.data = None + await async_update_entity(hass, "sensor.ha_version") + + assert mocker.data is None + assert state is not None + assert state.state == "Current Version: 2021.12.10" + + +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"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_class") + assert state.state == "['links']" + state2 = hass.states.get("sensor.ha_template") + assert state2.state == "Trying to get" + + +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"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", 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 diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 0058c05bf8084..f939142aee435 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,13 +1,44 @@ """Test the Sense config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from sense_energy import SenseAPITimeoutException, SenseAuthenticationException +import pytest +from sense_energy import ( + SenseAPITimeoutException, + SenseAuthenticationException, + SenseMFARequiredException, +) from homeassistant import config_entries from homeassistant.components.sense.const import DOMAIN +from homeassistant.const import CONF_CODE +from tests.common import MockConfigEntry -async def test_form(hass): +MOCK_CONFIG = { + "timeout": 6, + "email": "test-email", + "password": "test-password", + "access_token": "ABC", + "user_id": "123", + "monitor_id": "456", +} + + +@pytest.fixture(name="mock_sense") +def mock_sense(): + """Mock Sense object for authenticatation.""" + with patch( + "homeassistant.components.sense.config_flow.ASyncSenseable" + ) as mock_sense: + mock_sense.return_value.authenticate = AsyncMock(return_value=True) + mock_sense.return_value.validate_mfa = AsyncMock(return_value=True) + mock_sense.return_value.sense_access_token = "ABC" + mock_sense.return_value.sense_user_id = "123" + mock_sense.return_value.sense_monitor_id = "456" + yield mock_sense + + +async def test_form(hass, mock_sense): """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -16,7 +47,7 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch("sense_energy.ASyncSenseable.authenticate", return_value=True,), patch( + with patch( "homeassistant.components.sense.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -28,11 +59,7 @@ async def test_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "test-email" - assert result2["data"] == { - "timeout": 6, - "email": "test-email", - "password": "test-password", - } + assert result2["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -55,6 +82,113 @@ async def test_form_invalid_auth(hass): assert result2["errors"] == {"base": "invalid_auth"} +async def test_form_mfa_required(hass, mock_sense): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "validation" + + mock_sense.return_value.validate_mfa.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_CODE: "012345"}, + ) + + assert result3["type"] == "create_entry" + assert result3["title"] == "test-email" + assert result3["data"] == MOCK_CONFIG + + +async def test_form_mfa_required_wrong(hass, mock_sense): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "validation" + + mock_sense.return_value.validate_mfa.side_effect = SenseAuthenticationException + # Try with the WRONG verification code give us the form back again + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_CODE: "000000"}, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "invalid_auth"} + assert result3["step_id"] == "validation" + + +async def test_form_mfa_required_timeout(hass, mock_sense): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "validation" + + mock_sense.return_value.validate_mfa.side_effect = SenseAPITimeoutException + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_CODE: "000000"}, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + +async def test_form_mfa_required_exception(hass, mock_sense): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "validation" + + mock_sense.return_value.validate_mfa.side_effect = Exception + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_CODE: "000000"}, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "unknown"} + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -91,3 +225,57 @@ async def test_form_unknown_exception(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_no_form(hass, mock_sense): + """Test reauth where no form needed.""" + + # set up initially + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="test-email", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=MOCK_CONFIG + ) + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + + +async def test_reauth_password(hass, mock_sense): + """Test reauth form.""" + + # set up initially + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="test-email", + ) + entry.add_to_hass(hass) + mock_sense.return_value.authenticate.side_effect = SenseAuthenticationException + + # Reauth success without user input + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data + ) + assert result["type"] == "form" + + mock_sense.return_value.authenticate.side_effect = None + with patch( + "homeassistant.components.sense.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/senseme/test_config_flow.py b/tests/components/senseme/test_config_flow.py index 93a42d1e8ab11..e85845dcace40 100644 --- a/tests/components/senseme/test_config_flow.py +++ b/tests/components/senseme/test_config_flow.py @@ -211,7 +211,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) assert result["type"] == RESULT_TYPE_FORM @@ -254,7 +254,7 @@ async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> No with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) assert result["type"] == RESULT_TYPE_ABORT @@ -281,7 +281,7 @@ async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) await hass.async_block_till_done() diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index cf3716f09e404..9c59fc70763a6 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import aiohttp -from pysensibo import SensiboError +from pysensibo import AuthenticationError, SensiboError import pytest from homeassistant import config_entries @@ -123,6 +123,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: [ (aiohttp.ClientConnectionError), (asyncio.TimeoutError), + (AuthenticationError), (SensiboError), ], ) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index b49d8894932b3..df33cb1a08158 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -11,10 +11,14 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import State +from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.common import mock_restore_cache_with_extra_data + @pytest.mark.parametrize( "unit_system,native_unit,state_unit,native_value,state_value", @@ -165,71 +169,28 @@ async def test_datetime_conversion(hass, caplog, enable_custom_integrations): @pytest.mark.parametrize( - "device_class,native_value,state_value", + "device_class,state_value,provides", [ - (SensorDeviceClass.DATE, "2021-11-09", "2021-11-09"), - ( - SensorDeviceClass.DATE, - "2021-01-09T12:00:00+00:00", - "2021-01-09", - ), - ( - SensorDeviceClass.DATE, - "2021-01-09T00:00:00+01:00", - "2021-01-08", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09T12:00:00+00:00", - "2021-01-09T12:00:00+00:00", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09 12:00:00+00:00", - "2021-01-09T12:00:00+00:00", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09T12:00:00+04:00", - "2021-01-09T08:00:00+00:00", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09 12:00:00+01:00", - "2021-01-09T11:00:00+00:00", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09 12:00:00", - "2021-01-09T12:00:00", - ), - ( - SensorDeviceClass.TIMESTAMP, - "2021-01-09T12:00:00", - "2021-01-09T12:00:00", - ), + (SensorDeviceClass.DATE, "2021-01-09", "date"), + (SensorDeviceClass.TIMESTAMP, "2021-01-09T12:00:00+00:00", "datetime"), ], ) async def test_deprecated_datetime_str( - hass, caplog, enable_custom_integrations, device_class, native_value, state_value + hass, caplog, enable_custom_integrations, device_class, state_value, provides ): """Test warning on deprecated str for a date(time) value.""" platform = getattr(hass.components, "test.sensor") platform.init(empty=True) platform.ENTITIES["0"] = platform.MockSensor( - name="Test", native_value=native_value, device_class=device_class + name="Test", native_value=state_value, device_class=device_class ) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(entity0.entity_id) - assert state.state == state_value assert ( - "is providing a string for its state, while the device class is " - f"'{device_class}', this is not valid and will be unsupported " - "from Home Assistant 2022.2." + f"Invalid {provides}: sensor.test has a {device_class} device class " + f"but does not provide a {provides} state but {type(state_value)}" ) in caplog.text @@ -253,3 +214,131 @@ async def test_reject_timezoneless_datetime_str( "Invalid datetime: sensor.test provides state '2017-12-19 18:29:42', " "which is missing timezone information" ) in caplog.text + + +RESTORE_DATA = { + "str": {"native_unit_of_measurement": "°F", "native_value": "abc123"}, + "int": {"native_unit_of_measurement": "°F", "native_value": 123}, + "float": {"native_unit_of_measurement": "°F", "native_value": 123.0}, + "date": { + "native_unit_of_measurement": "°F", + "native_value": { + "__type": "", + "isoformat": date(2020, 2, 8).isoformat(), + }, + }, + "datetime": { + "native_unit_of_measurement": "°F", + "native_value": { + "__type": "", + "isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(), + }, + }, +} + + +# None | str | int | float | date | datetime: +@pytest.mark.parametrize( + "native_value, native_value_type, expected_extra_data, device_class", + [ + ("abc123", str, RESTORE_DATA["str"], None), + (123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE), + (123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE), + (date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE), + ( + datetime(2020, 2, 8, 15, tzinfo=timezone.utc), + dict, + RESTORE_DATA["datetime"], + SensorDeviceClass.TIMESTAMP, + ), + ], +) +async def test_restore_sensor_save_state( + hass, + enable_custom_integrations, + hass_storage, + native_value, + native_value_type, + expected_extra_data, + device_class, +): + """Test RestoreSensor.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockRestoreSensor( + name="Test", + native_value=native_value, + native_unit_of_measurement=TEMP_FAHRENHEIT, + device_class=device_class, + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"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 == expected_extra_data + assert type(extra_data["native_value"]) == native_value_type + + +@pytest.mark.parametrize( + "native_value, native_value_type, extra_data, device_class, uom", + [ + ("abc123", str, RESTORE_DATA["str"], None, "°F"), + (123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE, "°F"), + (123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE, "°F"), + (date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, "°F"), + ( + datetime(2020, 2, 8, 15, tzinfo=timezone.utc), + datetime, + RESTORE_DATA["datetime"], + SensorDeviceClass.TIMESTAMP, + "°F", + ), + (None, type(None), None, None, None), + (None, type(None), {}, None, None), + (None, type(None), {"beer": 123}, None, None), + ( + None, + type(None), + {"native_unit_of_measurement": "°F", "native_value": {}}, + None, + None, + ), + ], +) +async def test_restore_sensor_restore_state( + hass, + enable_custom_integrations, + hass_storage, + native_value, + native_value_type, + extra_data, + device_class, + uom, +): + """Test RestoreSensor.""" + mock_restore_cache_with_extra_data(hass, ((State("sensor.test", ""), extra_data),)) + + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockRestoreSensor( + name="Test", + device_class=device_class, + ) + + entity0 = platform.ENTITIES["0"] + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert hass.states.get(entity0.entity_id) + + assert entity0.native_value == native_value + assert type(entity0.native_value) == native_value_type + assert entity0.native_unit_of_measurement == uom diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 12690a35faa63..02e86ef03f84f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -19,6 +19,7 @@ } DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="shelly1pm-12345", port=None, diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py new file mode 100644 index 0000000000000..3669fd5a7fcaf --- /dev/null +++ b/tests/components/sleepiq/conftest.py @@ -0,0 +1,68 @@ +"""Common methods for SleepIQ.""" +from unittest.mock import create_autospec, patch + +from asyncsleepiq import SleepIQBed, SleepIQSleeper +import pytest + +from homeassistant.components.sleepiq import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +BED_ID = "123456" +BED_NAME = "Test Bed" +BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_") +SLEEPER_L_NAME = "SleeperL" +SLEEPER_R_NAME = "Sleeper R" +SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") +SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") + + +@pytest.fixture +def mock_asyncsleepiq(): + """Mock an AsyncSleepIQ object.""" + with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: + client = mock.return_value + bed = create_autospec(SleepIQBed) + client.beds = {BED_ID: bed} + bed.name = BED_NAME + bed.id = BED_ID + bed.mac_addr = "12:34:56:78:AB:CD" + bed.model = "C10" + bed.paused = False + sleeper_l = create_autospec(SleepIQSleeper) + sleeper_r = create_autospec(SleepIQSleeper) + bed.sleepers = [sleeper_l, sleeper_r] + + sleeper_l.side = "L" + sleeper_l.name = SLEEPER_L_NAME + sleeper_l.in_bed = True + sleeper_l.sleep_number = 40 + + sleeper_r.side = "R" + sleeper_r.name = SLEEPER_R_NAME + sleeper_r.in_bed = False + sleeper_r.sleep_number = 80 + + yield client + + +async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry: + """Set up the SleepIQ platform.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + }, + ) + mock_entry.add_to_hass(hass) + + if platform: + with patch("homeassistant.components.sleepiq.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index 9b4092d9d4837..2b265e1962679 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,45 +1,61 @@ """The tests for SleepIQ binary sensor platform.""" -from unittest.mock import MagicMock - -from homeassistant.components.sleepiq import binary_sensor as sleepiq -from homeassistant.setup import async_setup_component - -from tests.components.sleepiq.test_init import mock_responses - -CONFIG = {"username": "foo", "password": "bar"} - - -async def test_sensor_setup(hass, requests_mock): - """Test for successfully setting up the SleepIQ platform.""" - mock_responses(requests_mock) - - await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG}) - - device_mock = MagicMock() - sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock()) - devices = device_mock.call_args[0][0] - assert len(devices) == 2 - - left_side = devices[1] - assert left_side.name == "SleepNumber ILE Test1 Is In Bed" - assert left_side.state == "on" - - right_side = devices[0] - assert right_side.name == "SleepNumber ILE Test2 Is In Bed" - assert right_side.state == "off" - - -async def test_setup_single(hass, requests_mock): - """Test for successfully setting up the SleepIQ platform.""" - mock_responses(requests_mock, single=True) - - await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG}) - - device_mock = MagicMock() - sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock()) - devices = device_mock.call_args[0][0] - assert len(devices) == 1 - - right_side = devices[0] - assert right_side.name == "SleepNumber ILE Test1 Is In Bed" - assert right_side.state == "on" +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPER_R_NAME, + SLEEPER_R_NAME_LOWER, + setup_platform, +) + + +async def test_binary_sensors(hass, mock_asyncsleepiq): + """Test the SleepIQ binary sensors.""" + await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" + ) + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Is In Bed" + ) + + entity = entity_registry.async_get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" + ) + assert entity + assert entity.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_is_in_bed" + + state = hass.states.get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" + ) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ICON) == "mdi:bed-empty" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Is In Bed" + ) + + entity = entity_registry.async_get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" + ) + assert entity + assert entity.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_is_in_bed" diff --git a/tests/components/sleepiq/test_button.py b/tests/components/sleepiq/test_button.py new file mode 100644 index 0000000000000..cab3f36d73fba --- /dev/null +++ b/tests/components/sleepiq/test_button.py @@ -0,0 +1,61 @@ +"""The tests for SleepIQ binary sensor platform.""" +from homeassistant.components.button import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + setup_platform, +) + + +async def test_button_calibrate(hass, mock_asyncsleepiq): + """Test the SleepIQ calibrate button.""" + await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"button.sleepnumber_{BED_NAME_LOWER}_calibrate") + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Calibrate" + ) + + entity = entity_registry.async_get(f"button.sleepnumber_{BED_NAME_LOWER}_calibrate") + assert entity + assert entity.unique_id == f"{BED_ID}-calibrate" + + await hass.services.async_call( + DOMAIN, + "press", + {ATTR_ENTITY_ID: f"button.sleepnumber_{BED_NAME_LOWER}_calibrate"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].calibrate.assert_called_once() + + +async def test_button_stop_pump(hass, mock_asyncsleepiq): + """Test the SleepIQ stop pump button.""" + await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump") + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Stop Pump" + ) + + entity = entity_registry.async_get(f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump") + assert entity + assert entity.unique_id == f"{BED_ID}-stop-pump" + + await hass.services.async_call( + DOMAIN, + "press", + {ATTR_ENTITY_ID: f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].stop_pump.assert_called_once() diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py new file mode 100644 index 0000000000000..b2554ea968e63 --- /dev/null +++ b/tests/components/sleepiq/test_config_flow.py @@ -0,0 +1,91 @@ +"""Tests for the SleepIQ config flow.""" +from unittest.mock import patch + +from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.sleepiq.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +SLEEPIQ_CONFIG = { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", +} + + +async def test_import(hass: HomeAssistant) -> None: + """Test that we can import a config entry.""" + with patch("asyncsleepiq.AsyncSleepIQ.login"): + assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: SLEEPIQ_CONFIG}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data[CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] + assert entry.data[CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] + + +async def test_show_set_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + with patch("asyncsleepiq.AsyncSleepIQ.login"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_login_invalid_auth(hass: HomeAssistant) -> None: + """Test we show user form with appropriate error on login failure.""" + with patch( + "asyncsleepiq.AsyncSleepIQ.login", + side_effect=SleepIQLoginException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_login_cannot_connect(hass: HomeAssistant) -> None: + """Test we show user form with appropriate error on login failure.""" + with patch( + "asyncsleepiq.AsyncSleepIQ.login", + side_effect=SleepIQTimeoutException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_success(hass: HomeAssistant) -> None: + """Test successful flow provides entry creation data.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("asyncsleepiq.AsyncSleepIQ.login", return_value=True), patch( + "homeassistant.components.sleepiq.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], SLEEPIQ_CONFIG + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] + assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 68ca876504f91..0aed23c4c50bb 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,65 +1,66 @@ -"""The tests for the SleepIQ component.""" -from http import HTTPStatus -from unittest.mock import MagicMock, patch +"""Tests for the SleepIQ integration.""" +from asyncsleepiq import ( + SleepIQAPIException, + SleepIQLoginException, + SleepIQTimeoutException, +) -from homeassistant import setup -import homeassistant.components.sleepiq as sleepiq +from homeassistant.components.sleepiq.const import DOMAIN +from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import load_fixture +from tests.common import async_fire_time_changed +from tests.components.sleepiq.conftest import setup_platform -CONFIG = {"sleepiq": {"username": "foo", "password": "bar"}} +async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test unloading the SleepIQ entry.""" + entry = await setup_platform(hass, "sensor") + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() -def mock_responses(mock, single=False): - """Mock responses for SleepIQ.""" - base_url = "https://prod-api.sleepiq.sleepnumber.com/rest/" - if single: - suffix = "-single" - else: - suffix = "" - mock.put(base_url + "login", text=load_fixture("sleepiq-login.json")) - mock.get(base_url + "bed?_k=0987", text=load_fixture(f"sleepiq-bed{suffix}.json")) - mock.get(base_url + "sleeper?_k=0987", text=load_fixture("sleepiq-sleeper.json")) - mock.get( - base_url + "bed/familyStatus?_k=0987", - text=load_fixture(f"sleepiq-familystatus{suffix}.json"), - ) + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) -async def test_setup(hass, requests_mock): - """Test the setup.""" - mock_responses(requests_mock) +async def test_entry_setup_login_error(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client is unable to login.""" + mock_asyncsleepiq.login.side_effect = SleepIQLoginException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) - # We're mocking the load_platform discoveries or else the platforms - # will be setup during tear down when blocking till done, but the mocks - # are no longer active. - with patch("homeassistant.helpers.discovery.load_platform", MagicMock()): - assert sleepiq.setup(hass, CONFIG) +async def test_entry_setup_timeout_error( + hass: HomeAssistant, mock_asyncsleepiq +) -> None: + """Test when sleepiq client timeout.""" + mock_asyncsleepiq.login.side_effect = SleepIQTimeoutException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) -async def test_setup_login_failed(hass, requests_mock): - """Test the setup if a bad username or password is given.""" - mock_responses(requests_mock) - requests_mock.put( - "https://prod-api.sleepiq.sleepnumber.com/rest/login", - status_code=HTTPStatus.UNAUTHORIZED, - json=load_fixture("sleepiq-login-failed.json"), - ) - response = sleepiq.setup(hass, CONFIG) - assert not response +async def test_update_interval(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test update interval.""" + await setup_platform(hass, "sensor") + assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 1 + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() -async def test_setup_component_no_login(hass): - """Test the setup when no login is configured.""" - conf = CONFIG.copy() - del conf["sleepiq"]["username"] - assert not await setup.async_setup_component(hass, sleepiq.DOMAIN, conf) + assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 2 -async def test_setup_component_no_password(hass): - """Test the setup when no password is configured.""" - conf = CONFIG.copy() - del conf["sleepiq"]["password"] +async def test_api_error(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client is unable to login.""" + mock_asyncsleepiq.init_beds.side_effect = SleepIQAPIException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) - assert not await setup.async_setup_component(hass, sleepiq.DOMAIN, conf) + +async def test_api_timeout(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client timeout.""" + mock_asyncsleepiq.init_beds.side_effect = SleepIQTimeoutException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 7a7e47f03fa73..26ddc9aa485b2 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,48 +1,53 @@ """The tests for SleepIQ sensor platform.""" -from unittest.mock import MagicMock - -import homeassistant.components.sleepiq.sensor as sleepiq -from homeassistant.setup import async_setup_component - -from tests.components.sleepiq.test_init import mock_responses - -CONFIG = {"username": "foo", "password": "bar"} - - -async def test_setup(hass, requests_mock): - """Test for successfully setting up the SleepIQ platform.""" - mock_responses(requests_mock) - - assert await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG}) - - device_mock = MagicMock() - sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock()) - devices = device_mock.call_args[0][0] - assert len(devices) == 2 - - left_side = devices[1] - left_side.hass = hass - assert left_side.name == "SleepNumber ILE Test1 SleepNumber" - assert left_side.state == 40 - - right_side = devices[0] - right_side.hass = hass - assert right_side.name == "SleepNumber ILE Test2 SleepNumber" - assert right_side.state == 80 - - -async def test_setup_single(hass, requests_mock): - """Test for successfully setting up the SleepIQ platform.""" - mock_responses(requests_mock, single=True) - - assert await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG}) - - device_mock = MagicMock() - sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock()) - devices = device_mock.call_args[0][0] - assert len(devices) == 1 - - right_side = devices[0] - right_side.hass = hass - assert right_side.name == "SleepNumber ILE Test1 SleepNumber" - assert right_side.state == 40 +from homeassistant.components.sensor import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPER_R_NAME, + SLEEPER_R_NAME_LOWER, + setup_platform, +) + + +async def test_sensors(hass, mock_asyncsleepiq): + """Test the SleepIQ binary sensors for a bed with two sides.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" + ) + assert state.state == "40" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} SleepNumber" + ) + + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_sleep_number" + + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" + ) + assert state.state == "80" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} SleepNumber" + ) + + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number" diff --git a/tests/components/sleepiq/test_switch.py b/tests/components/sleepiq/test_switch.py new file mode 100644 index 0000000000000..38fc747c39d86 --- /dev/null +++ b/tests/components/sleepiq/test_switch.py @@ -0,0 +1,69 @@ +"""The tests for SleepIQ switch platform.""" +from homeassistant.components.sleepiq.coordinator import LONGER_UPDATE_INTERVAL +from homeassistant.components.switch import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + setup_platform, +) + + +async def test_setup(hass, mock_asyncsleepiq): + """Test for successfully setting up the SleepIQ platform.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + assert len(entity_registry.entities) == 1 + + entry = entity_registry.async_get(f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode") + assert entry + assert entry.original_name == f"SleepNumber {BED_NAME} Pause Mode" + assert entry.unique_id == f"{BED_ID}-pause-mode" + + +async def test_switch_set_states(hass, mock_asyncsleepiq): + """Test button press.""" + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + "turn_off", + {ATTR_ENTITY_ID: f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].set_pause_mode.assert_called_with(False) + + await hass.services.async_call( + DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].set_pause_mode.assert_called_with(True) + + +async def test_switch_get_states(hass, mock_asyncsleepiq): + """Test button press.""" + await setup_platform(hass, DOMAIN) + + assert ( + hass.states.get(f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode").state + == STATE_OFF + ) + mock_asyncsleepiq.beds[BED_ID].paused = True + + async_fire_time_changed(hass, utcnow() + LONGER_UPDATE_INTERVAL) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode").state + == STATE_ON + ) diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 58fafe930c7a6..9e4149b072035 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -4,6 +4,6 @@ async def test_sensors(hass, init_integration): """Test states of the sensors.""" - state = hass.states.get("sensor.grid_power") + state = hass.states.get("sensor.sma_device_grid_power") assert state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index f6f988a3caab4..16d330a21a8b4 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -57,6 +57,7 @@ async def test_show_zeroconf_connection_error_form(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee1006000212.local.", type="_ssh._tcp.local.", @@ -86,6 +87,7 @@ async def test_show_zeroconf_connection_error_form_next_generation(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee5001000212.local.", type="_ssh._tcp.local.", @@ -168,6 +170,7 @@ async def test_zeroconf_wrong_mdns(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="example.local.", type="_ssh._tcp.local.", @@ -278,6 +281,7 @@ async def test_zeroconf_device_exists_abort(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee1006000212.local.", type="_ssh._tcp.local.", @@ -327,6 +331,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee1006000212.local.", type="_ssh._tcp.local.", @@ -346,6 +351,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee1006000212.local.", type="_ssh._tcp.local.", @@ -465,6 +471,7 @@ async def test_full_zeroconf_flow(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee1006000212.local.", type="_ssh._tcp.local.", @@ -540,6 +547,7 @@ async def test_full_zeroconf_flow_next_generation(hass): context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], port=22, hostname="Smappee5001000212.local.", type="_ssh._tcp.local.", diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 8172cb4e0dd3e..ca9fc91bd5e55 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1,244 +1,9 @@ """Tests for the Sonarr component.""" -from http import HTTPStatus -from socket import gaierror as SocketGIAError -from unittest.mock import patch - -from homeassistant.components.sonarr.const import ( - CONF_BASE_PATH, - CONF_UPCOMING_DAYS, - CONF_WANTED_MAX_ITEMS, - DEFAULT_UPCOMING_DAYS, - DEFAULT_WANTED_MAX_ITEMS, - DOMAIN, -) -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONTENT_TYPE_JSON, -) -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry, load_fixture -from tests.test_util.aiohttp import AiohttpClientMocker - -HOST = "192.168.1.189" -PORT = 8989 -BASE_PATH = "/api" -API_KEY = "MOCK_API_KEY" +from homeassistant.const import CONF_API_KEY, CONF_URL MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"} MOCK_USER_INPUT = { - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_BASE_PATH: BASE_PATH, - CONF_SSL: False, - CONF_API_KEY: API_KEY, + CONF_URL: "http://192.168.1.189:8989", + CONF_API_KEY: "MOCK_API_KEY", } - - -def mock_connection( - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: str = PORT, - base_path: str = BASE_PATH, - error: bool = False, - invalid_auth: bool = False, - server_error: bool = False, -) -> None: - """Mock Sonarr connection.""" - if error: - mock_connection_error( - aioclient_mock, - host=host, - port=port, - base_path=base_path, - ) - return - - if invalid_auth: - mock_connection_invalid_auth( - aioclient_mock, - host=host, - port=port, - base_path=base_path, - ) - return - - if server_error: - mock_connection_server_error( - aioclient_mock, - host=host, - port=port, - base_path=base_path, - ) - return - - sonarr_url = f"http://{host}:{port}{base_path}" - - aioclient_mock.get( - f"{sonarr_url}/system/status", - text=load_fixture("sonarr/system-status.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/diskspace", - text=load_fixture("sonarr/diskspace.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/calendar", - text=load_fixture("sonarr/calendar.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/command", - text=load_fixture("sonarr/command.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/queue", - text=load_fixture("sonarr/queue.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/series", - text=load_fixture("sonarr/series.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - f"{sonarr_url}/wanted/missing", - text=load_fixture("sonarr/wanted-missing.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - -def mock_connection_error( - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: str = PORT, - base_path: str = BASE_PATH, -) -> None: - """Mock Sonarr connection errors.""" - sonarr_url = f"http://{host}:{port}{base_path}" - - aioclient_mock.get(f"{sonarr_url}/system/status", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/diskspace", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/calendar", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/command", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/queue", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/series", exc=SocketGIAError) - aioclient_mock.get(f"{sonarr_url}/missing/wanted", exc=SocketGIAError) - - -def mock_connection_invalid_auth( - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: str = PORT, - base_path: str = BASE_PATH, -) -> None: - """Mock Sonarr invalid auth errors.""" - sonarr_url = f"http://{host}:{port}{base_path}" - - aioclient_mock.get(f"{sonarr_url}/system/status", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/diskspace", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/calendar", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/command", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/queue", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/series", status=HTTPStatus.FORBIDDEN) - aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=HTTPStatus.FORBIDDEN) - - -def mock_connection_server_error( - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: str = PORT, - base_path: str = BASE_PATH, -) -> None: - """Mock Sonarr server errors.""" - sonarr_url = f"http://{host}:{port}{base_path}" - - aioclient_mock.get( - f"{sonarr_url}/system/status", status=HTTPStatus.INTERNAL_SERVER_ERROR - ) - aioclient_mock.get( - f"{sonarr_url}/diskspace", status=HTTPStatus.INTERNAL_SERVER_ERROR - ) - aioclient_mock.get( - f"{sonarr_url}/calendar", status=HTTPStatus.INTERNAL_SERVER_ERROR - ) - aioclient_mock.get(f"{sonarr_url}/command", status=HTTPStatus.INTERNAL_SERVER_ERROR) - aioclient_mock.get(f"{sonarr_url}/queue", status=HTTPStatus.INTERNAL_SERVER_ERROR) - aioclient_mock.get(f"{sonarr_url}/series", status=HTTPStatus.INTERNAL_SERVER_ERROR) - aioclient_mock.get( - f"{sonarr_url}/missing/wanted", status=HTTPStatus.INTERNAL_SERVER_ERROR - ) - - -async def setup_integration( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - host: str = HOST, - port: str = PORT, - base_path: str = BASE_PATH, - api_key: str = API_KEY, - unique_id: str = None, - skip_entry_setup: bool = False, - connection_error: bool = False, - invalid_auth: bool = False, - server_error: bool = False, -) -> MockConfigEntry: - """Set up the Sonarr integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id=unique_id, - data={ - CONF_HOST: host, - CONF_PORT: port, - CONF_BASE_PATH: base_path, - CONF_SSL: False, - CONF_VERIFY_SSL: False, - CONF_API_KEY: api_key, - CONF_UPCOMING_DAYS: DEFAULT_UPCOMING_DAYS, - CONF_WANTED_MAX_ITEMS: DEFAULT_WANTED_MAX_ITEMS, - }, - options={ - CONF_UPCOMING_DAYS: DEFAULT_UPCOMING_DAYS, - CONF_WANTED_MAX_ITEMS: DEFAULT_WANTED_MAX_ITEMS, - }, - ) - - entry.add_to_hass(hass) - - mock_connection( - aioclient_mock, - host=host, - port=port, - base_path=base_path, - error=connection_error, - invalid_auth=invalid_auth, - server_error=server_error, - ) - - if not skip_entry_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry - - -def _patch_async_setup_entry(return_value=True): - """Patch the async entry setup of sonarr.""" - return patch( - "homeassistant.components.sonarr.async_setup_entry", - return_value=return_value, - ) diff --git a/tests/components/sonarr/conftest.py b/tests/components/sonarr/conftest.py new file mode 100644 index 0000000000000..da8ff75df0fc6 --- /dev/null +++ b/tests/components/sonarr/conftest.py @@ -0,0 +1,156 @@ +"""Fixtures for Sonarr integration tests.""" +from collections.abc import Generator +import json +from unittest.mock import MagicMock, patch + +from aiopyarr import ( + Command, + Diskspace, + SonarrCalendar, + SonarrQueue, + SonarrSeries, + SonarrWantedMissing, + SystemStatus, +) +import pytest + +from homeassistant.components.sonarr.const import ( + CONF_BASE_PATH, + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DEFAULT_UPCOMING_DAYS, + DEFAULT_WANTED_MAX_ITEMS, + DOMAIN, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +def sonarr_calendar() -> list[SonarrCalendar]: + """Generate a response for the calendar method.""" + results = json.loads(load_fixture("sonarr/calendar.json")) + return [SonarrCalendar(result) for result in results] + + +def sonarr_commands() -> list[Command]: + """Generate a response for the commands method.""" + results = json.loads(load_fixture("sonarr/command.json")) + return [Command(result) for result in results] + + +def sonarr_diskspace() -> list[Diskspace]: + """Generate a response for the diskspace method.""" + results = json.loads(load_fixture("sonarr/diskspace.json")) + return [Diskspace(result) for result in results] + + +def sonarr_queue() -> SonarrQueue: + """Generate a response for the queue method.""" + results = json.loads(load_fixture("sonarr/queue.json")) + return SonarrQueue(results) + + +def sonarr_series() -> list[SonarrSeries]: + """Generate a response for the series method.""" + results = json.loads(load_fixture("sonarr/series.json")) + return [SonarrSeries(result) for result in results] + + +def sonarr_system_status() -> SystemStatus: + """Generate a response for the system status method.""" + result = json.loads(load_fixture("sonarr/system-status.json")) + return SystemStatus(result) + + +def sonarr_wanted() -> SonarrWantedMissing: + """Generate a response for the wanted method.""" + results = json.loads(load_fixture("sonarr/wanted-missing.json")) + return SonarrWantedMissing(results) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Sonarr", + domain=DOMAIN, + data={ + CONF_HOST: "192.168.1.189", + CONF_PORT: 8989, + CONF_BASE_PATH: "/api", + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_API_KEY: "MOCK_API_KEY", + CONF_UPCOMING_DAYS: DEFAULT_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS: DEFAULT_WANTED_MAX_ITEMS, + }, + options={ + CONF_UPCOMING_DAYS: DEFAULT_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS: DEFAULT_WANTED_MAX_ITEMS, + }, + unique_id=None, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.sonarr.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_sonarr_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Sonarr client.""" + with patch( + "homeassistant.components.sonarr.config_flow.SonarrClient", autospec=True + ) as sonarr_mock: + client = sonarr_mock.return_value + client.async_get_calendar.return_value = sonarr_calendar() + client.async_get_commands.return_value = sonarr_commands() + client.async_get_diskspace.return_value = sonarr_diskspace() + client.async_get_queue.return_value = sonarr_queue() + client.async_get_series.return_value = sonarr_series() + client.async_get_system_status.return_value = sonarr_system_status() + client.async_get_wanted.return_value = sonarr_wanted() + + yield client + + +@pytest.fixture +def mock_sonarr() -> Generator[None, MagicMock, None]: + """Return a mocked Sonarr client.""" + with patch( + "homeassistant.components.sonarr.SonarrClient", autospec=True + ) as sonarr_mock: + client = sonarr_mock.return_value + client.async_get_calendar.return_value = sonarr_calendar() + client.async_get_commands.return_value = sonarr_commands() + client.async_get_diskspace.return_value = sonarr_diskspace() + client.async_get_queue.return_value = sonarr_queue() + client.async_get_series.return_value = sonarr_series() + client.async_get_system_status.return_value = sonarr_system_status() + client.async_get_wanted.return_value = sonarr_wanted() + + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_sonarr: MagicMock +) -> MockConfigEntry: + """Set up the Sonarr integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/sonarr/fixtures/command.json b/tests/components/sonarr/fixtures/command.json index 97acc2f9f82e4..943bed3308cdd 100644 --- a/tests/components/sonarr/fixtures/command.json +++ b/tests/components/sonarr/fixtures/command.json @@ -17,7 +17,6 @@ "queued": "2020-04-06T16:54:06.41945Z", "started": "2020-04-06T16:54:06.421322Z", "trigger": "manual", - "state": "started", "manual": true, "startedOn": "2020-04-06T16:54:06.41945Z", "stateChangeTime": "2020-04-06T16:54:06.421322Z", @@ -27,7 +26,7 @@ }, { "name": "RefreshSeries", - "state": "started", + "status": "started", "startedOn": "2020-04-06T16:57:51.406504Z", "stateChangeTime": "2020-04-06T16:57:51.417931Z", "sendUpdatesToClient": true, diff --git a/tests/components/sonarr/fixtures/queue.json b/tests/components/sonarr/fixtures/queue.json index 1a8eb0924c33e..493353e2d88eb 100644 --- a/tests/components/sonarr/fixtures/queue.json +++ b/tests/components/sonarr/fixtures/queue.json @@ -1,129 +1,140 @@ -[ - { - "series": { - "title": "The Andy Griffith Show", - "sortTitle": "andy griffith show", - "seasonCount": 8, - "status": "ended", - "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", - "network": "CBS", - "airTime": "21:30", - "images": [ - { - "coverType": "fanart", - "url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" +{ + "page":1, + "pageSize":10, + "sortKey":"timeleft", + "sortDirection":"ascending", + "totalRecords":1, + "records":[ + { + "series":{ + "title":"The Andy Griffith Show", + "sortTitle":"andy griffith show", + "seasonCount":8, + "status":"ended", + "overview":"Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", + "network":"CBS", + "airTime":"21:30", + "images":[ + { + "coverType":"fanart", + "url":"https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" + }, + { + "coverType":"banner", + "url":"https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" + }, + { + "coverType":"poster", + "url":"https://artworks.thetvdb.com/banners/posters/77754-4.jpg" + } + ], + "seasons":[ + { + "seasonNumber":0, + "monitored":false + }, + { + "seasonNumber":1, + "monitored":false + }, + { + "seasonNumber":2, + "monitored":true + }, + { + "seasonNumber":3, + "monitored":false + }, + { + "seasonNumber":4, + "monitored":false + }, + { + "seasonNumber":5, + "monitored":true + }, + { + "seasonNumber":6, + "monitored":true + }, + { + "seasonNumber":7, + "monitored":true + }, + { + "seasonNumber":8, + "monitored":true + } + ], + "year":1960, + "path":"F:\\The Andy Griffith Show", + "profileId":5, + "seasonFolder":true, + "monitored":true, + "useSceneNumbering":false, + "runtime":25, + "tvdbId":77754, + "tvRageId":5574, + "tvMazeId":3853, + "firstAired":"1960-02-15T06:00:00Z", + "lastInfoSync":"2016-02-05T16:40:11.614176Z", + "seriesType":"standard", + "cleanTitle":"theandygriffithshow", + "imdbId":"", + "titleSlug":"the-andy-griffith-show", + "certification":"TV-G", + "genres":[ + "Comedy" + ], + "tags":[ + + ], + "added":"2008-02-04T13:44:24.204583Z", + "ratings":{ + "votes":547, + "value":8.6 }, - { - "coverType": "banner", - "url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" - }, - { - "coverType": "poster", - "url": "https://artworks.thetvdb.com/banners/posters/77754-4.jpg" - } - ], - "seasons": [ - { - "seasonNumber": 0, - "monitored": false - }, - { - "seasonNumber": 1, - "monitored": false - }, - { - "seasonNumber": 2, - "monitored": true - }, - { - "seasonNumber": 3, - "monitored": false - }, - { - "seasonNumber": 4, - "monitored": false - }, - { - "seasonNumber": 5, - "monitored": true - }, - { - "seasonNumber": 6, - "monitored": true - }, - { - "seasonNumber": 7, - "monitored": true + "qualityProfileId":5, + "id":17 + }, + "episode":{ + "seriesId":17, + "episodeFileId":0, + "seasonNumber":1, + "episodeNumber":1, + "title":"The New Housekeeper", + "airDate":"1960-10-03", + "airDateUtc":"1960-10-03T01:00:00Z", + "overview":"Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.", + "hasFile":false, + "monitored":false, + "absoluteEpisodeNumber":1, + "unverifiedSceneNumbering":false, + "id":889 + }, + "quality":{ + "quality":{ + "id":7, + "name":"SD" }, - { - "seasonNumber": 8, - "monitored": true + "revision":{ + "version":1, + "real":0 } - ], - "year": 1960, - "path": "F:\\The Andy Griffith Show", - "profileId": 5, - "seasonFolder": true, - "monitored": true, - "useSceneNumbering": false, - "runtime": 25, - "tvdbId": 77754, - "tvRageId": 5574, - "tvMazeId": 3853, - "firstAired": "1960-02-15T06:00:00Z", - "lastInfoSync": "2016-02-05T16:40:11.614176Z", - "seriesType": "standard", - "cleanTitle": "theandygriffithshow", - "imdbId": "", - "titleSlug": "the-andy-griffith-show", - "certification": "TV-G", - "genres": [ - "Comedy" - ], - "tags": [], - "added": "2008-02-04T13:44:24.204583Z", - "ratings": { - "votes": 547, - "value": 8.6 }, - "qualityProfileId": 5, - "id": 17 - }, - "episode": { - "seriesId": 17, - "episodeFileId": 0, - "seasonNumber": 1, - "episodeNumber": 1, - "title": "The New Housekeeper", - "airDate": "1960-10-03", - "airDateUtc": "1960-10-03T01:00:00Z", - "overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.", - "hasFile": false, - "monitored": false, - "absoluteEpisodeNumber": 1, - "unverifiedSceneNumbering": false, - "id": 889 - }, - "quality": { - "quality": { - "id": 7, - "name": "SD" - }, - "revision": { - "version": 1, - "real": 0 - } - }, - "size": 4472186820, - "title": "The.Andy.Griffith.Show.S01E01.x264-GROUP", - "sizeleft": 0, - "timeleft": "00:00:00", - "estimatedCompletionTime": "2016-02-05T22:46:52.440104Z", - "status": "Downloading", - "trackedDownloadStatus": "Ok", - "statusMessages": [], - "downloadId": "SABnzbd_nzo_Mq2f_b", - "protocol": "usenet", - "id": 1503378561 - } -] + "size":4472186820, + "title":"The.Andy.Griffith.Show.S01E01.x264-GROUP", + "sizeleft":0, + "timeleft":"00:00:00", + "estimatedCompletionTime":"2016-02-05T22:46:52.440104Z", + "status":"Downloading", + "trackedDownloadStatus":"Ok", + "statusMessages":[ + + ], + "downloadId":"SABnzbd_nzo_Mq2f_b", + "protocol":"usenet", + "id":1503378561 + } + ] +} diff --git a/tests/components/sonarr/fixtures/series.json b/tests/components/sonarr/fixtures/series.json index ea727c14a9765..154ab7eb75e21 100644 --- a/tests/components/sonarr/fixtures/series.json +++ b/tests/components/sonarr/fixtures/series.json @@ -3,11 +3,6 @@ "title": "The Andy Griffith Show", "alternateTitles": [], "sortTitle": "andy griffith show", - "seasonCount": 8, - "totalEpisodeCount": 253, - "episodeCount": 0, - "episodeFileCount": 0, - "sizeOnDisk": 0, "status": "ended", "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", "network": "CBS", @@ -158,6 +153,14 @@ "value": 8.6 }, "qualityProfileId": 2, + "statistics": { + "seasonCount": 8, + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 253, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + }, "id": 105 } ] diff --git a/tests/components/sonarr/fixtures/system-status.json b/tests/components/sonarr/fixtures/system-status.json index c3969df08fe5e..fe6198a044455 100644 --- a/tests/components/sonarr/fixtures/system-status.json +++ b/tests/components/sonarr/fixtures/system-status.json @@ -1,18 +1,29 @@ { - "version": "2.0.0.1121", - "buildTime": "2014-02-08T20:49:36.5560392Z", + "appName": "Sonarr", + "version": "3.0.6.1451", + "buildTime": "2022-01-23T16:51:56Z", "isDebug": false, "isProduction": true, - "isAdmin": true, + "isAdmin": false, "isUserInteractive": false, - "startupPath": "C:\\ProgramData\\NzbDrone\\bin", - "appData": "C:\\ProgramData\\NzbDrone", - "osVersion": "6.2.9200.0", - "isMono": false, - "isLinux": false, - "isWindows": true, + "startupPath": "/app/sonarr/bin", + "appData": "/config", + "osName": "ubuntu", + "osVersion": "20.04", + "isMonoRuntime": true, + "isMono": true, + "isLinux": true, + "isOsx": false, + "isWindows": false, + "mode": "console", "branch": "develop", - "authentication": false, - "startOfWeek": 0, - "urlBase": "" + "authentication": "forms", + "sqliteVersion": "3.31.1", + "urlBase": "", + "runtimeVersion": "6.12.0.122", + "runtimeName": "mono", + "startTime": "2022-02-01T22:10:11.956137Z", + "packageVersion": "3.0.6.1451-ls247", + "packageAuthor": "[linuxserver.io](https://linuxserver.io)", + "packageUpdateMechanism": "docker" } diff --git a/tests/components/sonarr/fixtures/wanted-missing.json b/tests/components/sonarr/fixtures/wanted-missing.json index 5db7c52f46995..df6212487fb38 100644 --- a/tests/components/sonarr/fixtures/wanted-missing.json +++ b/tests/components/sonarr/fixtures/wanted-missing.json @@ -1,6 +1,6 @@ { "page": 1, - "pageSize": 10, + "pageSize": 50, "sortKey": "airDateUtc", "sortDirection": "descending", "totalRecords": 2, diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 87b38e52742f5..59783995d23eb 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Sonarr config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from aiopyarr import ArrAuthenticationException, ArrException from homeassistant.components.sonarr.const import ( CONF_UPCOMING_DAYS, @@ -9,7 +11,7 @@ DOMAIN, ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL +from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -17,17 +19,8 @@ RESULT_TYPE_FORM, ) -from tests.components.sonarr import ( - HOST, - MOCK_REAUTH_INPUT, - MOCK_USER_INPUT, - _patch_async_setup_entry, - mock_connection, - mock_connection_error, - mock_connection_invalid_auth, - setup_integration, -) -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry +from tests.components.sonarr import MOCK_REAUTH_INPUT, MOCK_USER_INPUT async def test_show_user_form(hass: HomeAssistant) -> None: @@ -42,10 +35,10 @@ async def test_show_user_form(hass: HomeAssistant) -> None: async def test_cannot_connect( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_sonarr_config_flow: MagicMock ) -> None: """Test we show user form on connection error.""" - mock_connection_error(aioclient_mock) + mock_sonarr_config_flow.async_get_system_status.side_effect = ArrException user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -60,10 +53,12 @@ async def test_cannot_connect( async def test_invalid_auth( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_sonarr_config_flow: MagicMock ) -> None: """Test we show user form on invalid auth.""" - mock_connection_invalid_auth(aioclient_mock) + mock_sonarr_config_flow.async_get_system_status.side_effect = ( + ArrAuthenticationException + ) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -78,30 +73,30 @@ async def test_invalid_auth( async def test_unknown_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, mock_sonarr_config_flow: MagicMock ) -> None: """Test we show user form on unknown error.""" + mock_sonarr_config_flow.async_get_system_status.side_effect = Exception + user_input = MOCK_USER_INPUT.copy() - with patch( - "homeassistant.components.sonarr.config_flow.Sonarr.update", - side_effect=Exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - data=user_input, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, + ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "unknown" async def test_full_reauth_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_sonarr_config_flow: MagicMock, + mock_setup_entry: None, + init_integration: MockConfigEntry, ) -> None: """Test the manual reauth flow from start to finish.""" - entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) - assert entry + entry = init_integration result = await hass.config_entries.flow.async_init( DOMAIN, @@ -124,26 +119,23 @@ async def test_full_reauth_flow_implementation( assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() - with _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=user_input - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" - mock_setup_entry.assert_called_once() - async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_sonarr_config_flow: MagicMock, + mock_setup_entry: None, ) -> None: """Test the full manual user flow from start to finish.""" - mock_connection(aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -154,25 +146,24 @@ async def test_full_user_flow_implementation( user_input = MOCK_USER_INPUT.copy() - with _patch_async_setup_entry(): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST + assert result["title"] == "192.168.1.189" assert result["data"] - assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_URL] == "http://192.168.1.189:8989" async def test_full_user_flow_advanced_options( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_sonarr_config_flow: MagicMock, + mock_setup_entry: None, ) -> None: """Test the full manual user flow with advanced options.""" - mock_connection(aioclient_mock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) @@ -185,24 +176,27 @@ async def test_full_user_flow_advanced_options( CONF_VERIFY_SSL: True, } - with _patch_async_setup_entry(): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST + assert result["title"] == "192.168.1.189" assert result["data"] - assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_URL] == "http://192.168.1.189:8989" assert result["data"][CONF_VERIFY_SSL] -async def test_options_flow(hass, aioclient_mock: AiohttpClientMocker): +@patch("homeassistant.components.sonarr.PLATFORMS", []) +async def test_options_flow( + hass: HomeAssistant, + mock_setup_entry: None, + init_integration: MockConfigEntry, +): """Test updating options.""" - with patch("homeassistant.components.sonarr.PLATFORMS", []): - entry = await setup_integration(hass, aioclient_mock) + entry = init_integration assert entry.options[CONF_UPCOMING_DAYS] == DEFAULT_UPCOMING_DAYS assert entry.options[CONF_WANTED_MAX_ITEMS] == DEFAULT_WANTED_MAX_ITEMS @@ -212,12 +206,11 @@ async def test_options_flow(hass, aioclient_mock: AiohttpClientMocker): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" - with _patch_async_setup_entry(): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_UPCOMING_DAYS: 2, CONF_WANTED_MAX_ITEMS: 100}, - ) - await hass.async_block_till_done() + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_UPCOMING_DAYS: 2, CONF_WANTED_MAX_ITEMS: 100}, + ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 39d9b7fc24ea2..f4a317e3de0bf 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -1,60 +1,118 @@ """Tests for the Sonsrr integration.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from homeassistant.components.sonarr.const import DOMAIN +from aiopyarr import ArrAuthenticationException, ArrException + +from homeassistant.components.sonarr.const import CONF_BASE_PATH, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_SOURCE +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SOURCE, + CONF_SSL, + CONF_URL, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant -from tests.components.sonarr import setup_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry async def test_config_entry_not_ready( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sonarr: MagicMock, ) -> None: """Test the configuration entry not ready.""" - entry = await setup_integration(hass, aioclient_mock, connection_error=True) - assert entry.state is ConfigEntryState.SETUP_RETRY + mock_sonarr.async_get_system_status.side_effect = ArrException + + 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 mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_reauth( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sonarr: MagicMock, ) -> None: """Test the configuration entry needing to be re-authenticated.""" + mock_sonarr.async_get_system_status.side_effect = ArrAuthenticationException + with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: - entry = await setup_integration(hass, aioclient_mock, invalid_auth=True) + 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 entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_once_with( DOMAIN, context={ CONF_SOURCE: SOURCE_REAUTH, - "entry_id": entry.entry_id, - "unique_id": entry.unique_id, - "title_placeholders": {"name": entry.title}, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "title_placeholders": {"name": mock_config_entry.title}, }, - data=entry.data, + data=mock_config_entry.data, ) async def test_unload_config_entry( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sonarr: MagicMock, ) -> None: """Test the configuration entry unloading.""" + mock_config_entry.add_to_hass(hass) + with patch( "homeassistant.components.sonarr.sensor.async_setup_entry", return_value=True, ): - entry = await setup_integration(hass, aioclient_mock) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() assert hass.data[DOMAIN] - assert entry.entry_id in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_config_entry.entry_id in hass.data[DOMAIN] - await hass.config_entries.async_unload(entry.entry_id) + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state is ConfigEntryState.NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + + +async def test_migrate_config_entry(hass: HomeAssistant): + """Test successful migration of entry data.""" + legacy_config = { + CONF_API_KEY: "MOCK_API_KEY", + CONF_HOST: "1.2.3.4", + CONF_PORT: 8989, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_BASE_PATH: "/base/", + } + entry = MockConfigEntry(domain=DOMAIN, data=legacy_config) + + assert entry.data == legacy_config + assert entry.version == 1 + assert not entry.unique_id + + await entry.async_migrate(hass) + + assert entry.data == { + CONF_API_KEY: "MOCK_API_KEY", + CONF_HOST: "1.2.3.4", + CONF_PORT: 8989, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_BASE_PATH: "/base/", + CONF_URL: "http://1.2.3.4:8989/base", + } + assert entry.version == 2 + assert not entry.unique_id diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index f68920e4e4f15..c499dc0112f17 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,7 +1,8 @@ """Tests for the Sonarr sensor platform.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock, patch +from aiopyarr import ArrException import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -16,18 +17,18 @@ from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed -from tests.components.sonarr import mock_connection, setup_integration -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.common import MockConfigEntry, async_fire_time_changed UPCOMING_ENTITY_ID = f"{SENSOR_DOMAIN}.sonarr_upcoming" async def test_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sonarr: MagicMock, ) -> None: """Test the creation and values of the sensors.""" - entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) + entry = mock_config_entry registry = er.async_get(hass) # Pre-create registry entries for disabled by default sensors @@ -48,6 +49,7 @@ async def test_sensors( disabled_by=None, ) + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -66,30 +68,39 @@ async def test_sensors( assert state assert state.attributes.get(ATTR_ICON) == "mdi:harddisk" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get("C:\\") == "263.10/465.42GB (56.53%)" assert state.state == "263.10" state = hass.states.get("sensor.sonarr_queue") assert state assert state.attributes.get(ATTR_ICON) == "mdi:download" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.attributes.get("The Andy Griffith Show S01E01") == "100.00%" assert state.state == "1" state = hass.states.get("sensor.sonarr_shows") assert state assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Series" + assert state.attributes.get("The Andy Griffith Show") == "0/0 Episodes" assert state.state == "1" state = hass.states.get("sensor.sonarr_upcoming") assert state assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.attributes.get("Bob's Burgers") == "S04E11" assert state.state == "1" state = hass.states.get("sensor.sonarr_wanted") assert state assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-27T01:30:00+00:00" + assert ( + state.attributes.get("The Andy Griffith Show S01E01") + == "1960-10-03T01:00:00+00:00" + ) assert state.state == "2" @@ -104,10 +115,11 @@ async def test_sensors( ), ) async def test_disabled_by_default_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_id: str + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_id: str, ) -> None: """Test the disabled by default sensors.""" - await setup_integration(hass, aioclient_mock) registry = er.async_get(hass) state = hass.states.get(entity_id) @@ -120,56 +132,61 @@ async def test_disabled_by_default_sensors( async def test_availability( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sonarr: MagicMock, ) -> None: """Test entity availability.""" now = dt_util.utcnow() + mock_config_entry.add_to_hass(hass) with patch("homeassistant.util.dt.utcnow", return_value=now): - await setup_integration(hass, aioclient_mock) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" # state to unavailable - aioclient_mock.clear_requests() - mock_connection(aioclient_mock, error=True) + mock_sonarr.async_get_calendar.side_effect = ArrException future = now + timedelta(minutes=1) with patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE # state to available - aioclient_mock.clear_requests() - mock_connection(aioclient_mock) + mock_sonarr.async_get_calendar.side_effect = None future += timedelta(minutes=1) with patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" # state to unavailable - aioclient_mock.clear_requests() - mock_connection(aioclient_mock, invalid_auth=True) + mock_sonarr.async_get_calendar.side_effect = ArrException future += timedelta(minutes=1) with patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE # state to available - aioclient_mock.clear_requests() - mock_connection(aioclient_mock) + mock_sonarr.async_get_calendar.side_effect = None future += timedelta(minutes=1) with patch("homeassistant.util.dt.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert hass.states.get(UPCOMING_ENTITY_ID) assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index f3004ef22e2b1..d98ec4175fc24 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from songpal import SongpalException +from songpal.containers import Sysinfo from homeassistant.components.songpal.const import CONF_ENDPOINT from homeassistant.const import CONF_NAME @@ -12,6 +13,7 @@ ENDPOINT = f"http://{HOST}:10000/sony" MODEL = "model" MAC = "mac" +WIRELESS_MAC = "wmac" SW_VERSION = "sw_ver" CONF_DATA = { @@ -20,7 +22,7 @@ } -def _create_mocked_device(throw_exception=False): +def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=None): mocked_device = MagicMock() type(mocked_device).get_supported_methods = AsyncMock( @@ -35,9 +37,18 @@ def _create_mocked_device(throw_exception=False): return_value=interface_info ) - sys_info = MagicMock() - sys_info.macAddr = MAC - sys_info.version = SW_VERSION + sys_info = Sysinfo( + bdAddr=None, + macAddr=wired_mac, + wirelessMacAddr=wireless_mac, + bssid=None, + ssid=None, + bleID=None, + serialNumber=None, + generation=None, + model=None, + version=SW_VERSION, + ) type(mocked_device).get_system_info = AsyncMock(return_value=sys_info) volume1 = MagicMock() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 0fd5644e794ac..815995814f90b 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -29,6 +29,7 @@ MAC, MODEL, SW_VERSION, + WIRELESS_MAC, _create_mocked_device, _patch_media_player_device, ) @@ -126,6 +127,78 @@ async def test_state(hass): assert entity.unique_id == MAC +async def test_state_wireless(hass): + """Test state of the entity with only Wireless MAC.""" + mocked_device = _create_mocked_device(wired_mac=None, wireless_mac=WIRELESS_MAC) + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + attributes = state.as_dict()["attributes"] + assert attributes["volume_level"] == 0.5 + assert attributes["is_volume_muted"] is False + assert attributes["source_list"] == ["title1", "title2"] + assert attributes["source"] == "title2" + assert attributes["supported_features"] == SUPPORT_SONGPAL + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers={(songpal.DOMAIN, WIRELESS_MAC)} + ) + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, WIRELESS_MAC)} + assert device.manufacturer == "Sony Corporation" + assert device.name == FRIENDLY_NAME + assert device.sw_version == SW_VERSION + assert device.model == MODEL + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == WIRELESS_MAC + + +async def test_state_both(hass): + """Test state of the entity with both Wired and Wireless MAC.""" + mocked_device = _create_mocked_device(wired_mac=MAC, wireless_mac=WIRELESS_MAC) + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + attributes = state.as_dict()["attributes"] + assert attributes["volume_level"] == 0.5 + assert attributes["is_volume_muted"] is False + assert attributes["source_list"] == ["title1", "title2"] + assert attributes["source"] == "title2" + assert attributes["supported_features"] == SUPPORT_SONGPAL + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)}) + assert device.connections == { + (dr.CONNECTION_NETWORK_MAC, MAC), + (dr.CONNECTION_NETWORK_MAC, WIRELESS_MAC), + } + assert device.manufacturer == "Sony Corporation" + assert device.name == FRIENDLY_NAME + assert device.sw_version == SW_VERSION + assert device.model == MODEL + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(ENTITY_ID) + # We prefer the wired mac if present. + assert entity.unique_id == MAC + + async def test_services(hass): """Test services.""" mocked_device = _create_mocked_device() @@ -173,11 +246,7 @@ async def _call(service, **argv): mocked_device.set_sound_settings.assert_called_once_with("name", "value") mocked_device.set_sound_settings.reset_mock() - mocked_device2 = _create_mocked_device() - sys_info = MagicMock() - sys_info.macAddr = "mac2" - sys_info.version = SW_VERSION - type(mocked_device2).get_system_info = AsyncMock(return_value=sys_info) + mocked_device2 = _create_mocked_device(wired_mac="mac2") entry2 = MockConfigEntry( domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT} ) @@ -194,6 +263,27 @@ async def _call(service, **argv): ) mocked_device.set_sound_settings.assert_called_once_with("name", "value") mocked_device2.set_sound_settings.assert_called_once_with("name", "value") + mocked_device.set_sound_settings.reset_mock() + mocked_device2.set_sound_settings.reset_mock() + + mocked_device3 = _create_mocked_device(wired_mac=None, wireless_mac=WIRELESS_MAC) + entry3 = MockConfigEntry( + domain=songpal.DOMAIN, data={CONF_NAME: "d2", CONF_ENDPOINT: ENDPOINT} + ) + entry3.add_to_hass(hass) + with _patch_media_player_device(mocked_device3): + await hass.config_entries.async_setup(entry3.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + songpal.DOMAIN, + SET_SOUND_SETTING, + {"entity_id": "all", "name": "name", "value": "value"}, + blocking=True, + ) + mocked_device.set_sound_settings.assert_called_once_with("name", "value") + mocked_device2.set_sound_settings.assert_called_once_with("name", "value") + mocked_device3.set_sound_settings.assert_called_once_with("name", "value") async def test_websocket_events(hass): diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 14ad17bec8b65..8e133f76ac1c3 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from soco import SoCo from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -49,6 +50,7 @@ def zeroconf_payload(): """Return a default zeroconf payload.""" return zeroconf.ZeroconfServiceInfo( host="192.168.4.2", + addresses=["192.168.4.2"], hostname="Sonos-aaa", name="Sonos-aaa@Living Room._sonos._tcp.local.", port=None, @@ -82,7 +84,9 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): +def soco_fixture( + music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock +): """Create a mock soco SoCo fixture.""" with patch("homeassistant.components.sonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -92,6 +96,8 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.uid = "RINCON_test" mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library + mock_soco.get_current_track_info.return_value = current_track_info_empty + mock_soco.music_source_from_uri = SoCo.music_source_from_uri mock_soco.get_speaker_info.return_value = speaker_info mock_soco.avTransport = SonosMockService("AVTransport") mock_soco.renderingControl = SonosMockService("RenderingControl") @@ -106,6 +112,7 @@ def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): mock_soco.audio_delay = 2 mock_soco.bass = 1 mock_soco.treble = -1 + mock_soco.mic_enabled = False mock_soco.sub_enabled = False mock_soco.surround_enabled = True mock_soco.soundbar_audio_input_format = "Dolby 5.1" @@ -216,6 +223,22 @@ def speaker_info_fixture(): } +@pytest.fixture(name="current_track_info_empty") +def current_track_info_empty_fixture(): + """Create current_track_info_empty fixture.""" + return { + "title": "", + "artist": "", + "album": "", + "album_art": "", + "position": "NOT_IMPLEMENTED", + "playlist_position": "1", + "duration": "NOT_IMPLEMENTED", + "uri": "", + "metadata": "NOT_IMPLEMENTED", + } + + @pytest.fixture(name="battery_info") def battery_info_fixture(): """Create battery_info fixture.""" @@ -254,6 +277,63 @@ def alarm_event_fixture(soco): return SonosMockEvent(soco, soco.alarmClock, variables) +@pytest.fixture(name="no_media_event") +def no_media_event_fixture(soco): + """Create no_media_event_fixture.""" + variables = { + "current_crossfade_mode": "0", + "current_play_mode": "NORMAL", + "current_section": "0", + "current_track_meta_data": "", + "current_track_uri": "", + "enqueued_transport_uri": "", + "enqueued_transport_uri_meta_data": "", + "number_of_tracks": "0", + "transport_state": "STOPPED", + } + return SonosMockEvent(soco, soco.avTransport, variables) + + +@pytest.fixture(name="tv_event") +def tv_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "transport_state": "PLAYING", + "current_play_mode": "NORMAL", + "current_crossfade_mode": "0", + "number_of_tracks": "1", + "current_track": "1", + "current_section": "0", + "current_track_uri": f"x-sonos-htastream:{soco.uid}:spdif", + "current_track_duration": "", + "current_track_meta_data": { + "title": " ", + "parent_id": "-1", + "item_id": "-1", + "restricted": True, + "resources": [], + "desc": None, + }, + "next_track_uri": "", + "next_track_meta_data": "", + "enqueued_transport_uri": "", + "enqueued_transport_uri_meta_data": "", + "playback_storage_medium": "NETWORK", + "av_transport_uri": f"x-sonos-htastream:{soco.uid}:spdif", + "av_transport_uri_meta_data": { + "title": soco.uid, + "parent_id": "0", + "item_id": "spdif-input", + "restricted": False, + "resources": [], + "desc": None, + }, + "current_transport_actions": "Set, Play", + "current_valid_play_modes": "", + } + return SonosMockEvent(soco, soco.avTransport, variables) + + @pytest.fixture(autouse=True) def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index aa2ce6cc0be73..f0e6c81a41131 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -158,6 +158,7 @@ async def test_zeroconf_sonos_v1(hass: core.HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.107", + addresses=["192.168.1.107"], port=1443, hostname="sonos5CAAFDE47AC8.local.", type="_sonos._tcp.local.", diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 8fb757891496a..8a51ea5b2e6d4 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,9 +1,15 @@ """Tests for the Sonos battery sensor platform.""" +from unittest.mock import PropertyMock + from soco.exceptions import NotSupportedException +from homeassistant.components.sensor import SCAN_INTERVAL from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as ent_reg +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed async def test_entity_registry_unsupported(hass, async_setup_sonos, soco): @@ -113,27 +119,62 @@ async def test_device_payload_without_battery_and_ignored_keys( assert ignored_payload not in caplog.text -async def test_audio_input_sensor(hass, async_autosetup_sonos, soco): +async def test_audio_input_sensor( + hass, async_autosetup_sonos, soco, tv_event, no_media_event +): """Test audio input sensor.""" entity_registry = ent_reg.async_get(hass) + subscription = soco.avTransport.subscribe.return_value + sub_callback = subscription.callback + sub_callback(tv_event) + await hass.async_block_till_done() + audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) assert audio_input_state.state == "Dolby 5.1" + # Set mocked input format to new value and ensure poll success + no_input_mock = PropertyMock(return_value="No input") + type(soco).soundbar_audio_input_format = no_input_mock + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + no_input_mock.assert_called_once() + audio_input_state = hass.states.get(audio_input_sensor.entity_id) + assert audio_input_state.state == "No input" + + # Ensure state is not polled when source is not TV and state is already "No input" + sub_callback(no_media_event) + await hass.async_block_till_done() + + unpolled_mock = PropertyMock(return_value="Will not be polled") + type(soco).soundbar_audio_input_format = unpolled_mock + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + unpolled_mock.assert_not_called() + audio_input_state = hass.states.get(audio_input_sensor.entity_id) + assert audio_input_state.state == "No input" + async def test_microphone_binary_sensor( hass, async_autosetup_sonos, soco, device_properties_event ): """Test microphone binary sensor.""" entity_registry = ent_reg.async_get(hass) - assert "binary_sensor.zone_a_microphone" not in entity_registry.entities + assert "binary_sensor.zone_a_microphone" in entity_registry.entities + + mic_binary_sensor = entity_registry.entities["binary_sensor.zone_a_microphone"] + mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) + assert mic_binary_sensor_state.state == STATE_OFF # Update the speaker with a callback event subscription = soco.deviceProperties.subscribe.return_value subscription.callback(device_properties_event) await hass.async_block_till_done() - mic_binary_sensor = entity_registry.entities["binary_sensor.zone_a_microphone"] mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_ON diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index cb53fb43ed252..96b3d222dc61e 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -19,9 +19,7 @@ async def test_fallback_to_polling( caplog.clear() # Ensure subscriptions are cancelled and polling methods are called when subscriptions time out - with patch( - "homeassistant.components.sonos.speaker.SonosSpeaker.update_media" - ), patch( + with patch("homeassistant.components.sonos.media.SonosMedia.poll_media"), patch( "homeassistant.components.sonos.speaker.SonosSpeaker.subscription_address" ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index fb0279f911234..e2f1878c04d0a 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -15,6 +15,7 @@ BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( host="1.2.3.4", + addresses=["1.2.3.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -194,7 +195,13 @@ async def test_reauthentication( old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=old_entry.data + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, ) flows = hass.config_entries.flow.async_progress() @@ -261,7 +268,13 @@ async def test_reauth_account_mismatch( old_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=old_entry.data + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, ) flows = hass.config_entries.flow.async_progress() @@ -294,3 +307,13 @@ async def test_reauth_account_mismatch( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_account_mismatch" + + +async def test_abort_if_no_reauth_entry(hass): + """Check flow aborts when no entry is known when entring reauth confirmation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth_confirm"} + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 629ec464e58b0..0e543f98a2184 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,13 +1,27 @@ """The test for the sql sensor platform.""" +import os + import pytest import voluptuous as vol from homeassistant.components.sql.sensor import validate_sql_select from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import get_test_config_dir + + +@pytest.fixture(autouse=True) +def remove_file(): + """Remove db.""" + yield + file = os.path.join(get_test_config_dir(), "home-assistant_v2.db") + if os.path.isfile(file): + os.remove(file) -async def test_query(hass): + +async def test_query(hass: HomeAssistant) -> None: """Test the SQL sensor.""" config = { "sensor": { @@ -31,7 +45,53 @@ async def test_query(hass): assert state.attributes["value"] == 5 -async def test_query_limit(hass): +async def test_query_no_db(hass: HomeAssistant) -> None: + """Test the SQL sensor.""" + config = { + "sensor": { + "platform": "sql", + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value", + "column": "value", + } + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.count_tables") + assert state.state == "5" + + +async def test_query_value_template(hass: HomeAssistant) -> None: + """Test the SQL sensor.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5.01 as value", + "column": "value", + "value_template": "{{ value | int }}", + } + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.count_tables") + assert state.state == "5" + + +async def test_query_limit(hass: HomeAssistant) -> None: """Test the SQL sensor with a query containing 'LIMIT' in lowercase.""" config = { "sensor": { @@ -55,7 +115,30 @@ async def test_query_limit(hass): assert state.attributes["value"] == 5 -async def test_invalid_query(hass): +async def test_query_no_value(hass: HomeAssistant) -> None: + """Test the SQL sensor with a query that returns no value.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "queries": [ + { + "name": "count_tables", + "query": "SELECT 5 as value where 1=2", + "column": "value", + } + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.count_tables") + assert state.state == STATE_UNKNOWN + + +async def test_invalid_query(hass: HomeAssistant) -> None: """Test the SQL sensor for invalid queries.""" with pytest.raises(vol.Invalid): validate_sql_select("DROP TABLE *") @@ -81,6 +164,30 @@ async def test_invalid_query(hass): assert state.state == STATE_UNKNOWN +async def test_value_float_and_date(hass: HomeAssistant) -> None: + """Test the SQL sensor with a query has float as value.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "queries": [ + { + "name": "float_value", + "query": "SELECT 5 as value, cast(5.01 as decimal(10,2)) as value2", + "column": "value", + }, + ], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.float_value") + assert state.state == "5" + assert isinstance(state.attributes["value2"], float) + + @pytest.mark.parametrize( "url,expected_patterns,not_expected_patterns", [ @@ -96,7 +203,13 @@ async def test_invalid_query(hass): ), ], ) -async def test_invalid_url(hass, caplog, url, expected_patterns, not_expected_patterns): +async def test_invalid_url( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + url: str, + expected_patterns: str, + not_expected_patterns: str, +): """Test credentials in url is not logged.""" config = { "sensor": { diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 7876272368afe..ed887bb6049ff 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -211,7 +211,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_30303, ) await hass.async_block_till_done() @@ -249,7 +249,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_30303, ) await hass.async_block_till_done() @@ -339,7 +339,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( "source, data", [ (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), - (config_entries.SOURCE_DISCOVERY, DISCOVERY_30303), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, DISCOVERY_30303), ], ) async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( @@ -371,7 +371,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( "source, data", [ (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), - (config_entries.SOURCE_DISCOVERY, DISCOVERY_30303), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, DISCOVERY_30303), ], ) async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reload( diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index c754903965ab1..407b144267b28 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -16,7 +16,8 @@ from http import HTTPStatus import logging import threading -from unittest.mock import patch +from typing import Generator +from unittest.mock import Mock, patch from aiohttp import web import async_timeout @@ -219,6 +220,15 @@ def hls_sync(): yield sync +@pytest.fixture(autouse=True) +def should_retry() -> Generator[Mock, None, None]: + """Fixture to disable stream worker retries in tests by default.""" + with patch( + "homeassistant.components.stream._should_retry", return_value=False + ) as mock_should_retry: + yield mock_should_retry + + @pytest.fixture(scope="package") def h264_video(): """Generate a video, shared across tests.""" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 0492fec14f0fb..a2bb328826d46 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,6 +1,7 @@ """The tests for hls streams.""" from datetime import timedelta from http import HTTPStatus +import logging from unittest.mock import patch from urllib.parse import urlparse @@ -252,8 +253,8 @@ async def test_stream_timeout_after_stop( await hass.async_block_till_done() -async def test_stream_keepalive(hass, setup_component): - """Test hls stream retries the stream when keepalive=True.""" +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, {}) @@ -271,9 +272,11 @@ def update_callback() -> None: cur_time = 0 def time_side_effect(): + logging.info("time side effect") nonlocal cur_time if cur_time >= 80: - stream.keepalive = False # Thread should exit and be joinable. + logging.info("changing return value") + should_retry.return_value = False # Thread should exit and be joinable. cur_time += 40 return cur_time @@ -284,8 +287,8 @@ def time_side_effect(): ): av_open.side_effect = av.error.InvalidDataError(-2, "error") mock_time.time.side_effect = time_side_effect - # Request stream - stream.keepalive = True + # Request stream. Enable retries which are disabled by default in tests. + should_retry.return_value = True stream.start() stream._thread.join() stream._thread = None diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py new file mode 100644 index 0000000000000..92b2848caef9a --- /dev/null +++ b/tests/components/stream/test_init.py @@ -0,0 +1,46 @@ +"""Test stream init.""" + +import logging + +import av + +from homeassistant.components.stream import __name__ as stream_name +from homeassistant.setup import async_setup_component + + +async def test_log_levels(hass, caplog): + """Test that the worker logs the url without username and password.""" + + logging.getLogger(stream_name).setLevel(logging.INFO) + + await async_setup_component(hass, "stream", {"stream": {}}) + + # These namespaces should only pass log messages when the stream logger + # is at logging.DEBUG or below + namespaces_to_toggle = ( + "mp4", + "h264", + "hevc", + "rtsp", + "tcp", + "tls", + "mpegts", + "NULL", + ) + + # Since logging is at INFO, these should not pass + for namespace in namespaces_to_toggle: + av.logging.log(av.logging.ERROR, namespace, "SHOULD NOT PASS") + + logging.getLogger(stream_name).setLevel(logging.DEBUG) + + # Since logging is now at DEBUG, these should now pass + for namespace in namespaces_to_toggle: + av.logging.log(av.logging.ERROR, namespace, "SHOULD PASS") + + # Even though logging is at DEBUG, these should not pass + av.logging.log(av.logging.WARNING, "mp4", "SHOULD NOT PASS") + av.logging.log(av.logging.WARNING, "swscaler", "SHOULD NOT PASS") + + assert "SHOULD PASS" in caplog.text + assert "SHOULD NOT PASS" not in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index b54c8dc347244..e5477c66f524d 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -669,8 +669,8 @@ async def test_update_stream_source(hass): stream = Stream(hass, STREAM_SOURCE, {}) stream.add_provider(HLS_PROVIDER) - # Note that keepalive is not set here. The stream is "restarted" even though - # it is not stopping due to failure. + # Note that retries are disabled by default in tests, however the stream is "restarted" when + # the stream source is updated. py_av = MockPyAv() py_av.container.packets = PacketSequence(TEST_SEQUENCE_LENGTH) diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index f100fb53dc8a8..55c31da6c89a6 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -39,8 +39,11 @@ def setup_comp(hass): ) +@pytest.fixture(autouse=True) def teardown(): """Restore.""" + yield + dt_util.set_default_time_zone(ORIG_TIME_ZONE) diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index ecc36641e6c1c..94d116bbd36cb 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -30,6 +30,7 @@ FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", type="_system-bridge._udp.local.", @@ -47,6 +48,7 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", type="_system-bridge._udp.local.", diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 4c234fee2484d..8e30ff37de608 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -129,6 +129,7 @@ async def test_form_homekit(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, @@ -155,6 +156,7 @@ async def test_form_homekit(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 80bf14943a9fd..e88df08a80c73 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -392,7 +392,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async def call_service(hass, entity_id, service, **kwargs): """Call a fan service.""" await hass.services.async_call( - Platform.COVER, + cover.DOMAIN, service, {"entity_id": entity_id, **kwargs}, blocking=True, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 713d0f5ae67e3..90ca5d918fd2e 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -10,7 +10,7 @@ from .conftest import setup_tasmota_helper from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 -from tests.common import async_fire_mqtt_message +from tests.common import MockConfigEntry, async_fire_mqtt_message async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota): @@ -261,6 +261,111 @@ async def test_device_remove( assert device_entry is None +async def test_device_remove_multiple_config_entries_1( + hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota +): + """Test removing a discovered device.""" + config = copy.deepcopy(DEFAULT_CONFIG) + mac = config["mac"] + + mock_entry = MockConfigEntry(domain="test") + mock_entry.add_to_hass(hass) + + device_reg.async_get_or_create( + config_entry_id=mock_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, + ) + + tasmota_entry = hass.config_entries.async_entries("tasmota")[0] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) + assert device_entry is not None + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + "", + ) + await hass.async_block_till_done() + + # Verify device entry is not removed + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) + assert device_entry is not None + assert device_entry.config_entries == {mock_entry.entry_id} + + +async def test_device_remove_multiple_config_entries_2( + hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota +): + """Test removing a discovered device.""" + config = copy.deepcopy(DEFAULT_CONFIG) + mac = config["mac"] + + mock_entry = MockConfigEntry(domain="test") + mock_entry.add_to_hass(hass) + + device_reg.async_get_or_create( + config_entry_id=mock_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, + ) + + other_device_entry = device_reg.async_get_or_create( + config_entry_id=mock_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "other_device")}, + ) + + tasmota_entry = hass.config_entries.async_entries("tasmota")[0] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) + assert device_entry is not None + assert device_entry.config_entries == {tasmota_entry.entry_id, mock_entry.entry_id} + assert other_device_entry.id != device_entry.id + + # Remove other config entry from the device + device_reg.async_update_device( + device_entry.id, remove_config_entry_id=mock_entry.entry_id + ) + await hass.async_block_till_done() + + # Verify device entry is not removed + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) + assert device_entry is not None + assert device_entry.config_entries == {tasmota_entry.entry_id} + mqtt_mock.async_publish.assert_not_called() + + # Remove other config entry from the other device - Tasmota should not do any cleanup + device_reg.async_update_device( + other_device_entry.id, remove_config_entry_id=mock_entry.entry_id + ) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + + async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_tasmota): """Test removing a stale (undiscovered) device does not throw.""" mac = "00000049A3BC" diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 50249f54f03bc..f9422d60669cf 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -2,10 +2,16 @@ from unittest.mock import AsyncMock, MagicMock, patch -from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip +from kasa import SmartBulb, SmartDevice, SmartDimmer, SmartPlug, SmartStrip from kasa.exceptions import SmartDeviceException from kasa.protocol import TPLinkSmartHomeProtocol +from homeassistant.components.tplink import CONF_HOST +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + MODULE = "homeassistant.components.tplink" MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" IP_ADDRESS = "127.0.0.1" @@ -22,7 +28,7 @@ def _mock_protocol() -> TPLinkSmartHomeProtocol: def _mocked_bulb() -> SmartBulb: - bulb = MagicMock(auto_spec=SmartBulb) + bulb = MagicMock(auto_spec=SmartBulb, name="Mocked bulb") bulb.update = AsyncMock() bulb.mac = MAC_ADDRESS bulb.alias = ALIAS @@ -38,7 +44,7 @@ def _mocked_bulb() -> SmartBulb: bulb.device_id = MAC_ADDRESS bulb.valid_temperature_range.min = 4000 bulb.valid_temperature_range.max = 9000 - bulb.hw_info = {"sw_ver": "1.0.0"} + bulb.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} bulb.turn_off = AsyncMock() bulb.turn_on = AsyncMock() bulb.set_brightness = AsyncMock() @@ -49,10 +55,10 @@ def _mocked_bulb() -> SmartBulb: def _mocked_dimmer() -> SmartDimmer: - dimmer = MagicMock(auto_spec=SmartDimmer) + dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer") dimmer.update = AsyncMock() dimmer.mac = MAC_ADDRESS - dimmer.alias = ALIAS + dimmer.alias = "My Dimmer" dimmer.model = MODEL dimmer.host = IP_ADDRESS dimmer.brightness = 50 @@ -65,18 +71,19 @@ def _mocked_dimmer() -> SmartDimmer: dimmer.device_id = MAC_ADDRESS dimmer.valid_temperature_range.min = 4000 dimmer.valid_temperature_range.max = 9000 - dimmer.hw_info = {"sw_ver": "1.0.0"} + dimmer.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} dimmer.turn_off = AsyncMock() dimmer.turn_on = AsyncMock() dimmer.set_brightness = AsyncMock() dimmer.set_hsv = AsyncMock() dimmer.set_color_temp = AsyncMock() + dimmer.set_led = AsyncMock() dimmer.protocol = _mock_protocol() return dimmer def _mocked_plug() -> SmartPlug: - plug = MagicMock(auto_spec=SmartPlug) + plug = MagicMock(auto_spec=SmartPlug, name="Mocked plug") plug.update = AsyncMock() plug.mac = MAC_ADDRESS plug.alias = "My Plug" @@ -88,7 +95,7 @@ def _mocked_plug() -> SmartPlug: plug.is_strip = False plug.is_plug = True plug.device_id = MAC_ADDRESS - plug.hw_info = {"sw_ver": "1.0.0"} + plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} plug.turn_off = AsyncMock() plug.turn_on = AsyncMock() plug.set_led = AsyncMock() @@ -97,7 +104,7 @@ def _mocked_plug() -> SmartPlug: def _mocked_strip() -> SmartStrip: - strip = MagicMock(auto_spec=SmartStrip) + strip = MagicMock(auto_spec=SmartStrip, name="Mocked strip") strip.update = AsyncMock() strip.mac = MAC_ADDRESS strip.alias = "My Strip" @@ -109,7 +116,7 @@ def _mocked_strip() -> SmartStrip: strip.is_strip = True strip.is_plug = True strip.device_id = MAC_ADDRESS - strip.hw_info = {"sw_ver": "1.0.0"} + strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} strip.turn_off = AsyncMock() strip.turn_on = AsyncMock() strip.set_led = AsyncMock() @@ -146,3 +153,23 @@ async def _discover_single(*_): return patch( "homeassistant.components.tplink.Discover.discover_single", new=_discover_single ) + + +async def initialize_config_entry_for_device( + hass: HomeAssistant, dev: SmartDevice +) -> MockConfigEntry: + """Create a mocked configuration entry for the given device. + + Note, the rest of the tests should probably be converted over to use this + instead of repeating the initialization routine for each test separately + """ + config_entry = MockConfigEntry( + title="TP-Link", domain=DOMAIN, unique_id=dev.mac, data={CONF_HOST: dev.host} + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(device=dev), _patch_single_discovery(device=dev): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py deleted file mode 100644 index e579be61df23f..0000000000000 --- a/tests/components/tplink/consts.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Constants for the TP-Link component tests.""" - -SMARTPLUG_HS110_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS110(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM:ENE", - "mac": "69:F2:3C:8E:E3:47", - "updating": 0, - "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", - "next_action": {"type": -1}, - "err_code": 0, - }, - "realtime": { - "voltage_mv": 233957, - "current_ma": 21, - "power_mw": 0, - "total_wh": 1793, - "err_code": 0, - }, -} -SMARTPLUG_HS100_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS100(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM:", - "mac": "A9:F4:3D:A4:E3:47", - "updating": 0, - "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug", - "next_action": {"type": -1}, - "err_code": 0, - } -} -SMARTSTRIP_KP303_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 210428 Rel.135415", - "hw_ver": "1.0", - "model": "KP303(AU)", - "deviceId": "03102547AB1A57A4E4AA5B4EFE34C3005726B97D", - "oemId": "1F950FC9BFF278D9D35E046C129D9411", - "hwId": "9E86D4F840D2787D3D7A6523A731BA2C", - "rssi": -74, - "longitude_i": 1158985, - "latitude_i": -319172, - "alias": "TP-LINK_Power Strip_00B1", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM", - "mac": "D4:DD:D6:95:B0:F9", - "updating": 0, - "led_off": 0, - "children": [ - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913000", - "state": 0, - "alias": "R-Plug 1", - "on_time": 0, - "next_action": {"type": -1}, - }, - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913001", - "state": 1, - "alias": "R-Plug 2", - "on_time": 93835, - "next_action": {"type": -1}, - }, - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913002", - "state": 1, - "alias": "R-Plug 3", - "on_time": 93834, - "next_action": {"type": -1}, - }, - ], - "child_num": 3, - "err_code": 0, - }, - "realtime": { - "voltage_mv": 233957, - "current_ma": 21, - "power_mw": 0, - "total_wh": 1793, - "err_code": 0, - }, - "context": "1", -} diff --git a/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json b/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json new file mode 100644 index 0000000000000..4e3d4f01f2082 --- /dev/null +++ b/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json @@ -0,0 +1,108 @@ +{ + "device_last_response": { + "system": { + "get_sysinfo": { + "sw_ver": "1.8.8 Build 190613 Rel.123436", + "hw_ver": "1.0", + "model": "KL130(EU)", + "description": "Smart Wi-Fi LED Bulb with Color Changing", + "alias": "bedroom light", + "mic_type": "IOT.SMARTBULB", + "dev_state": "normal", + "mic_mac": "aa:bb:cc:dd:ee:ff", + "deviceId": "1234", + "oemId": "1234", + "hwId": "1234", + "is_factory": false, + "disco_ver": "1.0", + "ctrl_protocols": { + "name": "Linkie", + "version": "1.0" + }, + "light_state": { + "on_off": 1, + "mode": "normal", + "hue": 0, + "saturation": 0, + "color_temp": 2500, + "brightness": 58 + }, + "is_dimmable": 1, + "is_color": 1, + "is_variable_color_temp": 1, + "preferred_state": [ + { + "index": 0, + "hue": 0, + "saturation": 0, + "color_temp": 2500, + "brightness": 10 + }, + { + "index": 1, + "hue": 299, + "saturation": 95, + "color_temp": 0, + "brightness": 100 + }, + { + "index": 2, + "hue": 120, + "saturation": 75, + "color_temp": 0, + "brightness": 100 + }, + { + "index": 3, + "hue": 240, + "saturation": 75, + "color_temp": 0, + "brightness": 100 + } + ], + "rssi": -66, + "active_mode": "none", + "heapsize": 334532, + "err_code": 0 + } + }, + "smartlife.iot.common.emeter": { + "get_realtime": { + "power_mw": 6600, + "err_code": 0 + }, + "get_monthstat": { + "month_list": [ + { + "year": 2022, + "month": 1, + "energy_wh": 321 + }, + { + "year": 2022, + "month": 2, + "energy_wh": 321 + } + ], + "err_code": 0 + }, + "get_daystat": { + "day_list": [ + { + "year": 2022, + "month": 2, + "day": 1, + "energy_wh": 123 + }, + { + "year": 2022, + "month": 2, + "day": 2, + "energy_wh": 123 + } + ], + "err_code": 0 + } + } + } +} diff --git a/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json b/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json new file mode 100644 index 0000000000000..13dd14bbdda4b --- /dev/null +++ b/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json @@ -0,0 +1,74 @@ +{ + "device_last_response": { + "system": { + "get_sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "1234", + "oemId": "1234", + "hwId": "1234", + "rssi": -57, + "longitude_i": "0.0", + "latitude_i": "0.0", + "alias": "some plug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:ENE", + "mac": "aa:bb:cc:dd:ee:ff", + "updating": 0, + "led_off": 1, + "relay_state": 1, + "on_time": 254454, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": { + "type": -1 + }, + "err_code": 0 + } + }, + "emeter": { + "get_realtime": { + "voltage_mv": 230118, + "current_ma": 303, + "power_mw": 28825, + "total_wh": 18313, + "err_code": 0 + }, + "get_monthstat": { + "month_list": [ + { + "year": 2022, + "month": 2, + "energy_wh": 321 + }, + { + "year": 2022, + "month": 1, + "energy_wh": 321 + } + ], + "err_code": 0 + }, + "get_daystat": { + "day_list": [ + { + "year": 2022, + "month": 2, + "day": 1, + "energy_wh": 123 + }, + { + "year": 2022, + "month": 2, + "day": 2, + "energy_wh": 123 + } + ], + "err_code": 0 + } + } + } +} diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index fdc9fcea83ec8..a3792238fb281 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -252,7 +252,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): with _patch_discovery(), _patch_single_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ) await hass.async_block_till_done() @@ -304,7 +304,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), ), ( - config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_INTEGRATION_DISCOVERY, {CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ), ], @@ -345,7 +345,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=ALIAS), ), ( - config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_INTEGRATION_DISCOVERY, {CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ), ], diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py new file mode 100644 index 0000000000000..d09ea72b70a77 --- /dev/null +++ b/tests/components/tplink/test_diagnostics.py @@ -0,0 +1,60 @@ +"""Tests for the diagnostics data provided by the TP-Link integration.""" +import json + +from aiohttp import ClientSession +from kasa import SmartDevice +import pytest + +from homeassistant.core import HomeAssistant + +from . import _mocked_bulb, _mocked_plug, initialize_config_entry_for_device + +from tests.common import load_fixture +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.parametrize( + "mocked_dev,fixture_file,sysinfo_vars", + [ + ( + _mocked_bulb(), + "tplink-diagnostics-data-bulb-kl130.json", + ["mic_mac", "deviceId", "oemId", "hwId", "alias"], + ), + ( + _mocked_plug(), + "tplink-diagnostics-data-plug-hs110.json", + ["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"], + ), + ], +) +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mocked_dev: SmartDevice, + fixture_file: str, + sysinfo_vars: list[str], +): + """Test diagnostics for config entry.""" + diagnostics_data = json.loads(load_fixture(fixture_file, "tplink")) + + mocked_dev._last_update = diagnostics_data["device_last_response"] + + config_entry = await initialize_config_entry_for_device(hass, mocked_dev) + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert isinstance(result, dict) + assert "device_last_response" in result + + # There must be some redactions in place, so the raw data must not match + assert result["device_last_response"] != diagnostics_data["device_last_response"] + + last_response = result["device_last_response"] + + # We should always have sysinfo available + assert "system" in last_response + assert "get_sysinfo" in last_response["system"] + + sysinfo = last_response["system"]["get_sysinfo"] + for var in sysinfo_vars: + assert sysinfo[var] == "**REDACTED**" diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 03dc98f97991c..cafdcc6a54ff0 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock from kasa import SmartDeviceException +import pytest from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -12,10 +13,11 @@ 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 as dt_util +from homeassistant.util import dt as dt_util, slugify from . import ( MAC_ADDRESS, + _mocked_dimmer, _mocked_plug, _mocked_strip, _patch_discovery, @@ -53,36 +55,42 @@ async def test_plug(hass: HomeAssistant) -> None: plug.turn_on.reset_mock() -async def test_plug_led(hass: HomeAssistant) -> None: - """Test a smart plug LED.""" +@pytest.mark.parametrize( + "dev, domain", + [ + (_mocked_plug(), "switch"), + (_mocked_strip(), "switch"), + (_mocked_dimmer(), "light"), + ], +) +async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: + """Test LED setting for plugs, strips and dimmers.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - plug = _mocked_plug() - with _patch_discovery(device=plug), _patch_single_discovery(device=plug): + with _patch_discovery(device=dev), _patch_single_discovery(device=dev): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - entity_id = "switch.my_plug" - state = hass.states.get(entity_id) + entity_name = slugify(dev.alias) - led_entity_id = f"{entity_id}_led" + led_entity_id = f"switch.{entity_name}_led" led_state = hass.states.get(led_entity_id) assert led_state.state == STATE_ON - assert led_state.name == f"{state.name} LED" + assert led_state.name == f"{dev.alias} LED" await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - plug.set_led.assert_called_once_with(False) - plug.set_led.reset_mock() + dev.set_led.assert_called_once_with(False) + dev.set_led.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) - plug.set_led.assert_called_once_with(True) - plug.set_led.reset_mock() + dev.set_led.assert_called_once_with(True) + dev.set_led.reset_mock() async def test_plug_unique_id(hass: HomeAssistant) -> None: @@ -156,35 +164,6 @@ async def test_strip(hass: HomeAssistant) -> None: strip.children[plug_id].turn_on.reset_mock() -async def test_strip_led(hass: HomeAssistant) -> None: - """Test a smart strip LED.""" - already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS - ) - already_migrated_config_entry.add_to_hass(hass) - strip = _mocked_strip() - with _patch_discovery(device=strip), _patch_single_discovery(device=strip): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) - await hass.async_block_till_done() - - # We should have a LED entity for the strip - led_entity_id = "switch.my_strip_led" - led_state = hass.states.get(led_entity_id) - assert led_state.state == STATE_ON - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True - ) - strip.set_led.assert_called_once_with(False) - strip.set_led.reset_mock() - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True - ) - strip.set_led.assert_called_once_with(True) - strip.set_led.reset_mock() - - async def test_strip_unique_ids(hass: HomeAssistant) -> None: """Test a strip unique id.""" already_migrated_config_entry = MockConfigEntry( diff --git a/tests/components/tradfri/common.py b/tests/components/tradfri/common.py index 5e28bdcd55c4e..feeb60ab7c954 100644 --- a/tests/components/tradfri/common.py +++ b/tests/components/tradfri/common.py @@ -22,3 +22,5 @@ async def setup_integration(hass): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + + return entry diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index f4e871d79e10c..25f30237d0f55 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,5 +1,5 @@ """Common tradfri test fixtures.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch import pytest @@ -76,3 +76,20 @@ def mock_api_factory(mock_api): factory.init.return_value = factory.return_value factory.return_value.request = mock_api yield factory.return_value + + +@pytest.fixture(autouse=True) +def setup(request): + """ + Set up patches for pytradfri methods for the fan platform. + + This is used in test_fan as well as in test_sensor. + """ + with patch( + "pytradfri.device.AirPurifierControl.raw", + new_callable=PropertyMock, + return_value=[{"mock": "mock"}], + ), patch( + "pytradfri.device.AirPurifierControl.air_purifiers", + ): + yield diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 12726bc553aa6..90fce929f581f 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -106,6 +106,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + addresses=["123.123.123.123"], hostname="mock_hostname", name="mock_name", port=None, @@ -261,6 +262,7 @@ async def test_discovery_duplicate_aborted(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="new-host", + addresses=["new-host"], hostname="mock_hostname", name="mock_name", port=None, @@ -296,6 +298,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + addresses=["123.123.123.123"], hostname="mock_hostname", name="mock_name", port=None, @@ -311,6 +314,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="123.123.123.123", + addresses=["123.123.123.123"], hostname="mock_hostname", name="mock_name", port=None, @@ -335,6 +339,7 @@ async def test_discovery_updates_unique_id(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host="some-host", + addresses=["some-host"], hostname="mock_hostname", name="mock_name", port=None, diff --git a/tests/components/tradfri/test_diagnostics.py b/tests/components/tradfri/test_diagnostics.py new file mode 100644 index 0000000000000..d76e80a8b9c30 --- /dev/null +++ b/tests/components/tradfri/test_diagnostics.py @@ -0,0 +1,45 @@ +"""Tests for Tradfri diagnostics.""" +from unittest.mock import MagicMock, Mock + +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from .common import setup_integration +from .test_fan import mock_fan +from .test_light import mock_group + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mock_gateway: Mock, + mock_api_factory: MagicMock, +) -> None: + """Test diagnostics for config entry.""" + mock_gateway.mock_devices.append( + # Add a fan + mock_fan( + test_state={ + "fan_speed": 10, + "air_quality": 42, + "filter_lifetime_remaining": 120, + } + ) + ) + + mock_gateway.mock_groups.append( + # Add a group + mock_group(test_state={"state": True, "dimmer": 100}) + ) + + init_integration = await setup_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + + assert isinstance(result, dict) + assert result["gateway_version"] == "1.2.1234" + assert len(result["device_data"]) == 1 + assert result["no_of_groups"] == 1 diff --git a/tests/components/tradfri/test_fan.py b/tests/components/tradfri/test_fan.py index 13b7e59e10348..63e6a6558c964 100644 --- a/tests/components/tradfri/test_fan.py +++ b/tests/components/tradfri/test_fan.py @@ -1,6 +1,6 @@ """Tradfri fan (recognised as air purifiers in the IKEA ecosystem) platform tests.""" -from unittest.mock import MagicMock, Mock, PropertyMock, patch +from unittest.mock import MagicMock, Mock import pytest from pytradfri.device import Device @@ -10,19 +10,6 @@ from .common import setup_integration -@pytest.fixture(autouse=True, scope="module") -def setup(request): - """Set up patches for pytradfri methods.""" - with patch( - "pytradfri.device.AirPurifierControl.raw", - new_callable=PropertyMock, - return_value=[{"mock": "mock"}], - ), patch( - "pytradfri.device.AirPurifierControl.air_purifiers", - ): - yield - - def mock_fan(test_features=None, test_state=None, device_number=0): """Mock a tradfri fan/air purifier.""" if test_features is None: @@ -57,9 +44,7 @@ def mock_fan(test_features=None, test_state=None, device_number=0): async def test_fan(hass, mock_gateway, mock_api_factory): """Test that fans are correctly added.""" - state = { - "fan_speed": 10, - } + state = {"fan_speed": 10, "air_quality": 12} mock_gateway.mock_devices.append(mock_fan(test_state=state)) await setup_integration(hass) @@ -74,9 +59,7 @@ async def test_fan(hass, mock_gateway, mock_api_factory): async def test_fan_observed(hass, mock_gateway, mock_api_factory): """Test that fans are correctly observed.""" - state = { - "fan_speed": 10, - } + state = {"fan_speed": 10, "air_quality": 12} fan = mock_fan(test_state=state) mock_gateway.mock_devices.append(fan) @@ -87,10 +70,10 @@ async def test_fan_observed(hass, mock_gateway, mock_api_factory): async def test_fan_available(hass, mock_gateway, mock_api_factory): """Test fan available property.""" - fan = mock_fan(test_state={"fan_speed": 10}, device_number=1) + fan = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=1) fan.reachable = True - fan2 = mock_fan(test_state={"fan_speed": 10}, device_number=2) + fan2 = mock_fan(test_state={"fan_speed": 10, "air_quality": 12}, device_number=2) fan2.reachable = False mock_gateway.mock_devices.append(fan) @@ -120,8 +103,7 @@ async def test_set_percentage( ): """Test setting speed of a fan.""" # Note pytradfri style, not hass. Values not really important. - initial_state = {"percentage": 10, "fan_speed": 3} - + initial_state = {"percentage": 10, "fan_speed": 3, "air_quality": 12} # Setup the gateway with a mock fan. fan = mock_fan(test_state=initial_state, device_number=0) mock_gateway.mock_devices.append(fan) @@ -147,8 +129,8 @@ async def test_set_percentage( responses = mock_gateway.mock_responses mock_gateway_response = responses[0] - # A KeyError is raised if we don't add the 5908 response code - mock_gateway_response["15025"][0].update({"5908": 10}) + # A KeyError is raised if we don't this to the response code + mock_gateway_response["15025"][0].update({"5908": 10, "5907": 12, "5910": 20}) # Use the callback function to update the fan state. dev = Device(mock_gateway_response) diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index f96d1c09050d5..2a26391c43f84 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -49,3 +49,44 @@ async def test_entry_setup_unload(hass, mock_api_factory): await hass.async_block_till_done() assert unload.call_count == len(tradfri.PLATFORMS) assert mock_api_factory.shutdown.call_count == 1 + + +async def test_remove_stale_devices(hass, mock_api_factory): + """Test remove stale device registry entries.""" + entry = MockConfigEntry( + domain=tradfri.DOMAIN, + data={ + tradfri.CONF_HOST: "mock-host", + tradfri.CONF_IDENTITY: "mock-identity", + tradfri.CONF_KEY: "mock-key", + tradfri.CONF_IMPORT_GROUPS: True, + tradfri.CONF_GATEWAY_ID: GATEWAY_ID, + }, + ) + + entry.add_to_hass(hass) + dev_reg = dr.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(tradfri.DOMAIN, "stale_device_id")}, + ) + dev_entries = dr.async_entries_for_config_entry(dev_reg, entry.entry_id) + + assert len(dev_entries) == 1 + dev_entry = dev_entries[0] + assert dev_entry.identifiers == {(tradfri.DOMAIN, "stale_device_id")} + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + dev_entries = dr.async_entries_for_config_entry(dev_reg, entry.entry_id) + + # Check that only the gateway device entry remains. + assert len(dev_entries) == 1 + dev_entry = dev_entries[0] + assert dev_entry.identifiers == { + (tradfri.DOMAIN, entry.data[tradfri.CONF_GATEWAY_ID]) + } + assert dev_entry.manufacturer == tradfri.ATTR_TRADFRI_MANUFACTURER + assert dev_entry.name == tradfri.ATTR_TRADFRI_GATEWAY + assert dev_entry.model == tradfri.ATTR_TRADFRI_GATEWAY_MODEL diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 7de2c4dcb37fb..1ed24d7b080f4 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -317,6 +317,7 @@ def mock_group(test_state=None, group_number=0): _mock_group = Mock(member_ids=[], observe=Mock(), **state) _mock_group.name = f"tradfri_group_{group_number}" + _mock_group.id = group_number return _mock_group @@ -327,11 +328,11 @@ async def test_group(hass, mock_gateway, mock_api_factory): mock_gateway.mock_groups.append(mock_group(state, 1)) await setup_integration(hass) - group = hass.states.get("light.tradfri_group_0") + group = hass.states.get("light.tradfri_group_mock_gateway_id_0") assert group is not None assert group.state == "off" - group = hass.states.get("light.tradfri_group_1") + group = hass.states.get("light.tradfri_group_mock_gateway_id_1") assert group is not None assert group.state == "on" assert group.attributes["brightness"] == 100 @@ -349,18 +350,25 @@ async def test_group_turn_on(hass, mock_gateway, mock_api_factory): # Use the turn_off service call to change the light state. await hass.services.async_call( - "light", "turn_on", {"entity_id": "light.tradfri_group_0"}, blocking=True + "light", + "turn_on", + {"entity_id": "light.tradfri_group_mock_gateway_id_0"}, + blocking=True, ) await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.tradfri_group_1", "brightness": 100}, + {"entity_id": "light.tradfri_group_mock_gateway_id_1", "brightness": 100}, blocking=True, ) await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.tradfri_group_2", "brightness": 100, "transition": 1}, + { + "entity_id": "light.tradfri_group_mock_gateway_id_2", + "brightness": 100, + "transition": 1, + }, blocking=True, ) await hass.async_block_till_done() @@ -378,7 +386,10 @@ async def test_group_turn_off(hass, mock_gateway, mock_api_factory): # Use the turn_off service call to change the light state. await hass.services.async_call( - "light", "turn_off", {"entity_id": "light.tradfri_group_0"}, blocking=True + "light", + "turn_off", + {"entity_id": "light.tradfri_group_mock_gateway_id_0"}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 1e9ae71828534..a36ff93a60774 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -1,20 +1,25 @@ """Tradfri sensor platform tests.""" +from __future__ import annotations from unittest.mock import MagicMock, Mock +from homeassistant.components import tradfri +from homeassistant.helpers import entity_registry as er + +from . import GATEWAY_ID from .common import setup_integration +from .test_fan import mock_fan + +from tests.common import MockConfigEntry -def mock_sensor(state_name: str, state_value: str, device_number=0): +def mock_sensor(test_state: list, device_number=0): """Mock a tradfri sensor.""" dev_info_mock = MagicMock() dev_info_mock.manufacturer = "manufacturer" dev_info_mock.model_number = "model" dev_info_mock.firmware_version = "1.2.3" - # Set state value, eg battery_level = 50 - setattr(dev_info_mock, state_name, state_value) - _mock_sensor = Mock( id=f"mock-sensor-id-{device_number}", reachable=True, @@ -26,6 +31,11 @@ def mock_sensor(state_name: str, state_value: str, device_number=0): has_signal_repeater_control=False, has_air_purifier_control=False, ) + + # Set state value, eg battery_level = 50, or has_air_purifier_control=True + for state in test_state: + setattr(dev_info_mock, state["attribute"], state["value"]) + _mock_sensor.name = f"tradfri_sensor_{device_number}" return _mock_sensor @@ -34,7 +44,7 @@ def mock_sensor(state_name: str, state_value: str, device_number=0): async def test_battery_sensor(hass, mock_gateway, mock_api_factory): """Test that a battery sensor is correctly added.""" mock_gateway.mock_devices.append( - mock_sensor(state_name="battery_level", state_value=60) + mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}]) ) await setup_integration(hass) @@ -45,10 +55,66 @@ async def test_battery_sensor(hass, mock_gateway, mock_api_factory): assert sensor_1.attributes["device_class"] == "battery" +async def test_cover_battery_sensor(hass, mock_gateway, mock_api_factory): + """Test that a battery sensor is correctly added for a cover (blind).""" + mock_gateway.mock_devices.append( + mock_sensor( + test_state=[ + {"attribute": "battery_level", "value": 42, "has_blind_control": True} + ] + ) + ) + await setup_integration(hass) + + sensor_1 = hass.states.get("sensor.tradfri_sensor_0") + assert sensor_1 is not None + assert sensor_1.state == "42" + assert sensor_1.attributes["unit_of_measurement"] == "%" + assert sensor_1.attributes["device_class"] == "battery" + + +async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory): + """Test that a battery sensor is correctly added.""" + mock_gateway.mock_devices.append( + mock_fan( + test_state={ + "fan_speed": 10, + "air_quality": 42, + "filter_lifetime_remaining": 120, + } + ) + ) + await setup_integration(hass) + + sensor_1 = hass.states.get("sensor.tradfri_fan_0_air_quality") + assert sensor_1 is not None + assert sensor_1.state == "42" + assert sensor_1.attributes["unit_of_measurement"] == "µg/m³" + assert sensor_1.attributes["device_class"] == "aqi" + + +async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory): + """Test that a battery sensor is correctly added.""" + mock_gateway.mock_devices.append( + mock_fan( + test_state={ + "fan_speed": 10, + "air_quality": 42, + "filter_lifetime_remaining": 120, + } + ) + ) + await setup_integration(hass) + + sensor_1 = hass.states.get("sensor.tradfri_fan_0_filter_time_left") + assert sensor_1 is not None + assert sensor_1.state == "2" + assert sensor_1.attributes["unit_of_measurement"] == "h" + + async def test_sensor_observed(hass, mock_gateway, mock_api_factory): """Test that sensors are correctly observed.""" - - sensor = mock_sensor(state_name="battery_level", state_value=60) + sensor = mock_sensor(test_state=[{"attribute": "battery_level", "value": 60}]) mock_gateway.mock_devices.append(sensor) await setup_integration(hass) assert len(sensor.observe.mock_calls) > 0 @@ -56,11 +122,14 @@ async def test_sensor_observed(hass, mock_gateway, mock_api_factory): async def test_sensor_available(hass, mock_gateway, mock_api_factory): """Test sensor available property.""" - - sensor = mock_sensor(state_name="battery_level", state_value=60, device_number=1) + sensor = mock_sensor( + test_state=[{"attribute": "battery_level", "value": 60}], device_number=1 + ) sensor.reachable = True - sensor2 = mock_sensor(state_name="battery_level", state_value=60, device_number=2) + sensor2 = mock_sensor( + test_state=[{"attribute": "battery_level", "value": 60}], device_number=2 + ) sensor2.reachable = False mock_gateway.mock_devices.append(sensor) @@ -69,3 +138,51 @@ async def test_sensor_available(hass, mock_gateway, mock_api_factory): assert hass.states.get("sensor.tradfri_sensor_1").state == "60" assert hass.states.get("sensor.tradfri_sensor_2").state == "unavailable" + + +async def test_unique_id_migration(hass, mock_gateway, mock_api_factory): + """Test unique ID is migrated from old format to new.""" + ent_reg = er.async_get(hass) + old_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0" + entry = MockConfigEntry( + domain=tradfri.DOMAIN, + data={ + "host": "mock-host", + "identity": "mock-identity", + "key": "mock-key", + "import_groups": False, + "gateway_id": GATEWAY_ID, + }, + ) + entry.add_to_hass(hass) + + # Version 1 + sensor_name = "sensor.tradfri_sensor_0" + entity_name = sensor_name.split(".")[1] + + entity_entry = ent_reg.async_get_or_create( + "sensor", + tradfri.DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == sensor_name + assert entity_entry.unique_id == old_unique_id + + # Add a sensor to the gateway so that it populates coordinator list + sensor = mock_sensor( + test_state=[{"attribute": "battery_level", "value": 60}], + ) + mock_gateway.mock_devices.append(sensor) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(sensor_name) + new_unique_id = f"{GATEWAY_ID}-mock-sensor-id-0-battery_level" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", tradfri.DOMAIN, old_unique_id) is None diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index 3580880fedbce..6d99597839168 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -2,9 +2,12 @@ From http://doc.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures """ +from unittest.mock import patch import pytest +from homeassistant.components.tts import _get_cache_files + @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -16,3 +19,55 @@ def pytest_runtest_makereport(item, call): # set a report attribute for each phase of a call, which can # be "setup", "call", "teardown" setattr(item, f"rep_{rep.when}", rep) + + +@pytest.fixture(autouse=True) +def mock_get_cache_files(): + """Mock the list TTS cache function.""" + with patch( + "homeassistant.components.tts._get_cache_files", return_value={} + ) as mock_cache_files: + yield mock_cache_files + + +@pytest.fixture(autouse=True) +def mock_init_cache_dir(): + """Mock the TTS cache dir in memory.""" + with patch( + "homeassistant.components.tts._init_tts_cache_dir", + side_effect=lambda hass, cache_dir: hass.config.path(cache_dir), + ) as mock_cache_dir: + yield mock_cache_dir + + +@pytest.fixture +def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files, request): + """Mock the TTS cache dir with empty dir.""" + mock_init_cache_dir.side_effect = None + mock_init_cache_dir.return_value = str(tmp_path) + + # Restore original get cache files behavior, we're working with a real dir. + mock_get_cache_files.side_effect = _get_cache_files + + yield tmp_path + + if request.node.rep_call.passed: + return + + # Print contents of dir if failed + print("Content of dir for", request.node.nodeid) + for fil in tmp_path.iterdir(): + print(fil.relative_to(tmp_path)) + + # To show the log. + assert False + + +@pytest.fixture(autouse=True) +def mutagen_mock(): + """Mock writing tags.""" + with patch( + "homeassistant.components.tts.SpeechManager.write_tags", + side_effect=lambda *args: args[1], + ) as mock_write_tags: + yield mock_write_tags diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 3cbc1f0da00c7..9f1cc849a1f65 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -5,6 +5,7 @@ import pytest import yarl +from homeassistant.components import tts from homeassistant.components.demo.tts import DemoProvider from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -13,13 +14,13 @@ MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -import homeassistant.components.tts as tts -from homeassistant.components.tts import _get_cache_files from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_mock_service +ORIG_WRITE_TAGS = tts.SpeechManager.write_tags + def relative_url(url): """Convert an absolute url to a relative one.""" @@ -32,58 +33,6 @@ def demo_provider(): return DemoProvider("en") -@pytest.fixture(autouse=True) -def mock_get_cache_files(): - """Mock the list TTS cache function.""" - with patch( - "homeassistant.components.tts._get_cache_files", return_value={} - ) as mock_cache_files: - yield mock_cache_files - - -@pytest.fixture(autouse=True) -def mock_init_cache_dir(): - """Mock the TTS cache dir in memory.""" - with patch( - "homeassistant.components.tts._init_tts_cache_dir", - side_effect=lambda hass, cache_dir: hass.config.path(cache_dir), - ) as mock_cache_dir: - yield mock_cache_dir - - -@pytest.fixture -def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files, request): - """Mock the TTS cache dir with empty dir.""" - mock_init_cache_dir.side_effect = None - mock_init_cache_dir.return_value = str(tmp_path) - - # Restore original get cache files behavior, we're working with a real dir. - mock_get_cache_files.side_effect = _get_cache_files - - yield tmp_path - - if request.node.rep_call.passed: - return - - # Print contents of dir if failed - print("Content of dir for", request.node.nodeid) - for fil in tmp_path.iterdir(): - print(fil.relative_to(tmp_path)) - - # To show the log. - assert False - - -@pytest.fixture() -def mutagen_mock(): - """Mock writing tags.""" - with patch( - "homeassistant.components.tts.SpeechManager.write_tags", - side_effect=lambda *args: args[1], - ): - yield - - @pytest.fixture(autouse=True) async def internal_url_mock(hass): """Mock internal URL of the instance.""" @@ -730,7 +679,7 @@ async def test_tags_with_wave(hass, demo_provider): + "22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 00 00 00" ) - tagged_data = tts.SpeechManager.write_tags( + tagged_data = ORIG_WRITE_TAGS( "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.wav", demo_data, demo_provider, diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py new file mode 100644 index 0000000000000..22edfef535855 --- /dev/null +++ b/tests/components/tts/test_media_source.py @@ -0,0 +1,113 @@ +"""Tests for TTS media source.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +async def mock_get_tts_audio(hass): + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + assert await async_setup_component( + hass, + "tts", + { + "tts": { + "platform": "demo", + } + }, + ) + + with patch( + "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", + return_value=("mp3", b""), + ) as mock_get_tts: + yield mock_get_tts + + +async def test_browsing(hass): + """Test browsing TTS media source.""" + item = await media_source.async_browse_media(hass, "media-source://tts") + assert item is not None + assert item.title == "Text to Speech" + assert len(item.children) == 1 + assert item.can_play is False + assert item.can_expand is True + + item_child = await media_source.async_browse_media( + hass, item.children[0].media_content_id + ) + assert item_child is not None + assert item_child.media_content_id == item.children[0].media_content_id + assert item_child.title == "Demo" + assert item_child.children is None + assert item_child.can_play is False + assert item_child.can_expand is True + + item_child = await media_source.async_browse_media( + hass, item.children[0].media_content_id + "?message=bla" + ) + assert item_child is not None + assert ( + item_child.media_content_id + == item.children[0].media_content_id + "?message=bla" + ) + assert item_child.title == "Demo" + assert item_child.children is None + assert item_child.can_play is False + assert item_child.can_expand is True + + with pytest.raises(BrowseError): + await media_source.async_browse_media(hass, "media-source://tts/non-existing") + + +async def test_resolving(hass, mock_get_tts_audio): + """Test resolving.""" + media = await media_source.async_resolve_media( + hass, "media-source://tts/demo?message=Hello%20World" + ) + assert media.url.startswith("/api/tts_proxy/") + assert media.mime_type == "audio/mpeg" + + assert len(mock_get_tts_audio.mock_calls) == 1 + message, language = mock_get_tts_audio.mock_calls[0][1] + assert message == "Hello World" + assert language == "en" + assert mock_get_tts_audio.mock_calls[0][2]["options"] is None + + # Pass language and options + mock_get_tts_audio.reset_mock() + media = await media_source.async_resolve_media( + hass, "media-source://tts/demo?message=Bye%20World&language=de&voice=Paulus" + ) + assert media.url.startswith("/api/tts_proxy/") + assert media.mime_type == "audio/mpeg" + + assert len(mock_get_tts_audio.mock_calls) == 1 + message, language = mock_get_tts_audio.mock_calls[0][1] + assert message == "Bye World" + assert language == "de" + assert mock_get_tts_audio.mock_calls[0][2]["options"] == {"voice": "Paulus"} + + +async def test_resolving_errors(hass): + """Test resolving.""" + # No message added + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media(hass, "media-source://tts/demo") + + # Non-existing provider + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media( + hass, "media-source://tts/non-existing?message=bla" + ) + + # Non-existing option + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media( + hass, "media-source://tts/non-existing?message=bla&non_existing_option=bla" + ) diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 9989b1d349e34..912896dd3e229 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -1,6 +1,4 @@ """The tests for the TTS component.""" -from unittest.mock import patch - import pytest import yarl @@ -22,16 +20,6 @@ def relative_url(url): return str(yarl.URL(url).relative()) -@pytest.fixture(autouse=True) -def mutagen_mock(): - """Mock writing tags.""" - with patch( - "homeassistant.components.tts.SpeechManager.write_tags", - side_effect=lambda *args: args[1], - ): - yield - - @pytest.fixture(autouse=True) async def internal_url_mock(hass): """Mock internal URL of the instance.""" diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 310be91c7961c..bfffeb4ae7f1a 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,11 +1,8 @@ """The tests for an update of the Twitch component.""" from unittest.mock import MagicMock, patch -from requests import HTTPError -from twitch.resources import Channel, Follow, Stream, Subscription, User - from homeassistant.components import sensor -from homeassistant.const import CONF_CLIENT_ID +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component ENTITY_ID = "sensor.channel123" @@ -13,6 +10,7 @@ sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: " abcd", "channels": ["channel123"], } } @@ -20,39 +18,46 @@ sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: "abcd", "channels": ["channel123"], "token": "9876", } } -USER_ID = User({"id": 123, "display_name": "channel123", "logo": "logo.png"}) -STREAM_OBJECT_ONLINE = Stream( - { - "channel": {"game": "Good Game", "status": "Title"}, - "preview": {"medium": "stream-medium.png"}, - } -) -CHANNEL_OBJECT = Channel({"followers": 42, "views": 24}) -OAUTH_USER_ID = User({"id": 987}) -SUB_ACTIVE = Subscription({"created_at": "2020-01-20T21:22:42", "is_gift": False}) -FOLLOW_ACTIVE = Follow({"created_at": "2020-01-20T21:22:42"}) +USER_OBJECT = { + "id": 123, + "display_name": "channel123", + "offline_image_url": "logo.png", + "view_count": 42, +} +STREAM_OBJECT_ONLINE = { + "game_name": "Good Game", + "title": "Title", + "thumbnail_url": "stream-medium.png", +} + +FOLLOWERS_OBJECT = [{"followed_at": "2020-01-20T21:22:42"}] * 24 +OAUTH_USER_ID = {"id": 987} +SUB_ACTIVE = {"is_gift": False} +FOLLOW_ACTIVE = {"followed_at": "2020-01-20T21:22:42"} + + +def make_data(data): + """Create a data object.""" + return {"data": data, "total": len(data)} async def test_init(hass): """Test initial config.""" - channels = MagicMock() - channels.get_by_id.return_value = CHANNEL_OBJECT - streams = MagicMock() - streams.get_stream_by_user.return_value = None - twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels = channels - twitch_mock.streams = streams + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True await hass.async_block_till_done() @@ -62,20 +67,21 @@ async def test_init(hass): assert sensor_state.name == "channel123" assert sensor_state.attributes["icon"] == "mdi:twitch" assert sensor_state.attributes["friendly_name"] == "channel123" - assert sensor_state.attributes["views"] == 24 - assert sensor_state.attributes["followers"] == 42 + assert sensor_state.attributes["views"] == 42 + assert sensor_state.attributes["followers"] == 24 async def test_offline(hass): """Test offline state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = None + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -90,12 +96,13 @@ async def test_streaming(hass): """Test streaming state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = STREAM_OBJECT_ONLINE + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.get_streams.return_value = make_data([STREAM_OBJECT_ONLINE]) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -112,15 +119,21 @@ async def test_oauth_without_sub_and_follow(hass): """Test state with oauth.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -135,15 +148,23 @@ async def test_oauth_with_sub(hass): """Test state with oauth and sub.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.return_value = SUB_ACTIVE - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + + # This function does not return an array so use make_data + twitch_mock.check_user_subscription.return_value = make_data([SUB_ACTIVE]) with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -151,7 +172,6 @@ async def test_oauth_with_sub(hass): sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is True - assert sensor_state.attributes["subscribed_since"] == "2020-01-20T21:22:42" assert sensor_state.attributes["subscription_is_gifted"] is False assert sensor_state.attributes["following"] is False @@ -160,15 +180,21 @@ async def test_oauth_with_follow(hass): """Test state with oauth and follow.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.return_value = FOLLOW_ACTIVE + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([FOLLOW_ACTIVE]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index b490d43fffdf5..532f19c35ae1d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -54,23 +54,6 @@ async def test_tracked_wireless_clients( assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME - # State change signalling works without events - - mock_unifi_websocket( - data={ - "meta": {"message": MESSAGE_CLIENT}, - "data": [client], - } - ) - await hass.async_block_till_done() - - client_state = hass.states.get("device_tracker.client") - assert client_state.state == STATE_NOT_HOME - assert client_state.attributes["ip"] == "10.0.0.1" - assert client_state.attributes["mac"] == "00:00:00:00:00:01" - assert client_state.attributes["hostname"] == "client" - assert client_state.attributes["host_name"] == "client" - # Updated timestamp marks client as home client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -93,7 +76,7 @@ async def test_tracked_wireless_clients( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME - # Same timestamp again means client is away + # Same timestamp doesn't explicitly mark client as away mock_unifi_websocket( data={ @@ -103,7 +86,7 @@ async def test_tracked_wireless_clients( ) await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + assert hass.states.get("device_tracker.client").state == STATE_HOME async def test_tracked_clients( diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 68d90ff82eb12..4c7f12a69fa02 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -302,7 +302,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -371,7 +371,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -407,7 +407,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -439,7 +439,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -457,7 +457,7 @@ async def test_discovered_by_unifi_discovery( with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -509,7 +509,7 @@ async def test_discovered_by_unifi_discovery_partial( with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT_PARTIAL, ) await hass.async_block_till_done() @@ -574,7 +574,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -604,7 +604,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() @@ -642,7 +642,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=other_ip_dict, ) await hass.async_block_till_done() @@ -678,7 +678,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=other_ip_dict, ) await hass.async_block_till_done() @@ -747,7 +747,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa with _patch_discovery(), patch.object(hass.loop, "getaddrinfo", return_value=[]): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=other_ip_dict, ) await hass.async_block_till_done() @@ -768,7 +768,7 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant, mock_nvr: NVR) -> N with _patch_discovery(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DISCOVERY}, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=UNIFI_DISCOVERY_DICT, ) await hass.async_block_till_done() diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py new file mode 100644 index 0000000000000..e7ea6ee6d6efe --- /dev/null +++ b/tests/components/vallox/conftest.py @@ -0,0 +1,65 @@ +"""Common utilities for Vallox tests.""" + +import random +import string +from typing import Any +from unittest.mock import patch +from uuid import UUID + +import pytest +from vallox_websocket_api.vallox import PROFILE + +from homeassistant.components.vallox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create mocked Vallox config entry.""" + vallox_mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.100.50", + CONF_NAME: "Vallox", + }, + ) + vallox_mock_entry.add_to_hass(hass) + + return vallox_mock_entry + + +def patch_metrics(metrics: dict[str, Any]): + """Patch the Vallox metrics response.""" + return patch( + "homeassistant.components.vallox.Vallox.fetch_metrics", + return_value=metrics, + ) + + +@pytest.fixture(autouse=True) +def patch_profile_home(): + """Patch the Vallox profile response.""" + with patch( + "homeassistant.components.vallox.Vallox.get_profile", + return_value=PROFILE.HOME, + ): + yield + + +@pytest.fixture(autouse=True) +def patch_uuid(): + """Patch the Vallox entity UUID.""" + with patch( + "homeassistant.components.vallox.calculate_uuid", + return_value=_random_uuid(), + ): + yield + + +def _random_uuid(): + """Generate a random UUID.""" + uuid = "".join(random.choices(string.hexdigits, k=32)) + return UUID(uuid) diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py new file mode 100644 index 0000000000000..bd8ecbea905e5 --- /dev/null +++ b/tests/components/vallox/test_sensor.py @@ -0,0 +1,175 @@ +"""Tests for Vallox sensor platform.""" + +from datetime import datetime, timedelta, tzinfo +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .conftest import patch_metrics + +from tests.common import MockConfigEntry + +ORIG_TZ = dt.DEFAULT_TIME_ZONE + + +@pytest.fixture(autouse=True) +def reset_tz(): + """Restore the default TZ after test runs.""" + yield + dt.DEFAULT_TIME_ZONE = ORIG_TZ + + +@pytest.fixture +def set_tz(request): + """Set the default TZ to the one requested.""" + return request.getfixturevalue(request.param) + + +@pytest.fixture +def utc() -> tzinfo: + """Set the default TZ to UTC.""" + tz = dt.get_time_zone("UTC") + dt.set_default_time_zone(tz) + return tz + + +@pytest.fixture +def helsinki() -> tzinfo: + """Set the default TZ to Europe/Helsinki.""" + tz = dt.get_time_zone("Europe/Helsinki") + dt.set_default_time_zone(tz) + return tz + + +@pytest.fixture +def new_york() -> tzinfo: + """Set the default TZ to America/New_York.""" + tz = dt.get_time_zone("America/New_York") + dt.set_default_time_zone(tz) + return tz + + +def _sensor_to_datetime(sensor): + return datetime.fromisoformat(sensor.state) + + +def _now_at_13(): + return dt.now().timetz().replace(hour=13, minute=0, second=0, microsecond=0) + + +async def test_remaining_filter_returns_timestamp( + mock_entry: MockConfigEntry, hass: HomeAssistant +): + """Test that the remaining time for filter sensor returns a timestamp.""" + # Act + with patch( + "homeassistant.components.vallox.calculate_next_filter_change_date", + return_value=dt.now().date(), + ), patch_metrics(metrics={}): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_remaining_time_for_filter") + assert sensor.attributes["device_class"] == "timestamp" + + +async def test_remaining_time_for_filter_none_returned_from_vallox( + mock_entry: MockConfigEntry, hass: HomeAssistant +): + """Test that the remaining time for filter sensor returns 'unknown' when Vallox returns None.""" + # Act + with patch( + "homeassistant.components.vallox.calculate_next_filter_change_date", + return_value=None, + ), patch_metrics(metrics={}): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_remaining_time_for_filter") + assert sensor.state == "unknown" + + +@pytest.mark.parametrize( + "set_tz", + [ + "utc", + "helsinki", + "new_york", + ], + indirect=True, +) +async def test_remaining_time_for_filter_in_the_future( + mock_entry: MockConfigEntry, set_tz: tzinfo, hass: HomeAssistant +): + """Test remaining time for filter when Vallox returns a date in the future.""" + # Arrange + remaining_days = 112 + mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days) + + # Act + with patch( + "homeassistant.components.vallox.calculate_next_filter_change_date", + return_value=mocked_filter_end_date, + ), patch_metrics(metrics={}): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_remaining_time_for_filter") + assert _sensor_to_datetime(sensor) == datetime.combine( + mocked_filter_end_date, + _now_at_13(), + ) + + +async def test_remaining_time_for_filter_today( + mock_entry: MockConfigEntry, hass: HomeAssistant +): + """Test remaining time for filter when Vallox returns today.""" + # Arrange + remaining_days = 0 + mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days) + + # Act + with patch( + "homeassistant.components.vallox.calculate_next_filter_change_date", + return_value=mocked_filter_end_date, + ), patch_metrics(metrics={}): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_remaining_time_for_filter") + assert _sensor_to_datetime(sensor) == datetime.combine( + mocked_filter_end_date, + _now_at_13(), + ) + + +async def test_remaining_time_for_filter_in_the_past( + mock_entry: MockConfigEntry, hass: HomeAssistant +): + """Test remaining time for filter when Vallox returns a date in the past.""" + # Arrange + remaining_days = -3 + mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days) + + # Act + with patch( + "homeassistant.components.vallox.calculate_next_filter_change_date", + return_value=mocked_filter_end_date, + ), patch_metrics(metrics={}): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get("sensor.vallox_remaining_time_for_filter") + assert _sensor_to_datetime(sensor) == datetime.combine( + mocked_filter_end_date, + _now_at_13(), + ) diff --git a/tests/components/version/common.py b/tests/components/version/common.py index 17d72d6de72d5..b210a8600b8b2 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -52,9 +52,17 @@ async def mock_get_version_update( await hass.async_block_till_done() -async def setup_version_integration(hass: HomeAssistant) -> MockConfigEntry: +async def setup_version_integration( + hass: HomeAssistant, + entry_data: dict[str, Any] | None = None, +) -> MockConfigEntry: """Set up the Version integration.""" - mock_entry = MockConfigEntry(**MOCK_VERSION_CONFIG_ENTRY_DATA) + mock_entry = MockConfigEntry( + **{ + **MOCK_VERSION_CONFIG_ENTRY_DATA, + "data": entry_data or MOCK_VERSION_CONFIG_ENTRY_DATA["data"], + } + ) mock_entry.add_to_hass(hass) with patch( @@ -65,7 +73,6 @@ async def setup_version_integration(hass: HomeAssistant) -> MockConfigEntry: assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("sensor.local_installation").state == MOCK_VERSION assert mock_entry.state == config_entries.ConfigEntryState.LOADED return mock_entry diff --git a/tests/components/version/test_binary_sensor.py b/tests/components/version/test_binary_sensor.py new file mode 100644 index 0000000000000..c9551ad441569 --- /dev/null +++ b/tests/components/version/test_binary_sensor.py @@ -0,0 +1,23 @@ +"""The test for the version binary sensor platform.""" +from __future__ import annotations + +from homeassistant.components.version.const import DEFAULT_CONFIGURATION +from homeassistant.core import HomeAssistant + +from .common import setup_version_integration + + +async def test_version_binary_sensor_local_source(hass: HomeAssistant): + """Test the Version binary sensor with local source.""" + await setup_version_integration(hass) + + state = hass.states.get("binary_sensor.local_installation_update_available") + assert not state + + +async def test_version_binary_sensor(hass: HomeAssistant): + """Test the Version binary sensor.""" + await setup_version_integration(hass, {**DEFAULT_CONFIGURATION, "source": "pypi"}) + + state = hass.states.get("binary_sensor.local_installation_update_available") + assert state diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index b4a3fc047664c..119443962fced 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -1,9 +1,8 @@ """Constants for the Vizio integration tests.""" from homeassistant.components import zeroconf from homeassistant.components.media_player import ( - DEVICE_CLASS_SPEAKER, - DEVICE_CLASS_TV, DOMAIN as MP_DOMAIN, + MediaPlayerDeviceClass, ) from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, @@ -102,7 +101,7 @@ def __init__(self, auth_token: str) -> None: MOCK_USER_VALID_TV_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, } @@ -113,7 +112,7 @@ def __init__(self, auth_token: str) -> None: MOCK_IMPORT_VALID_TV_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, } @@ -121,7 +120,7 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_WITH_INCLUDE_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]}, @@ -130,7 +129,7 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_WITH_EXCLUDE_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, CONF_APPS: {CONF_EXCLUDE: ["Netflix"]}, @@ -139,7 +138,7 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]}, @@ -148,7 +147,7 @@ def __init__(self, auth_token: str) -> None: MOCK_SPEAKER_APPS_FAILURE = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.SPEAKER, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]}, @@ -157,7 +156,7 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_APPS_FAILURE = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_VOLUME_STEP: VOLUME_STEP, CONF_APPS: None, @@ -165,7 +164,7 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_APPS_WITH_VALID_APPS_CONFIG = { CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, CONF_ACCESS_TOKEN: ACCESS_TOKEN, CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]}, } @@ -173,13 +172,13 @@ def __init__(self, auth_token: str) -> None: MOCK_TV_CONFIG_NO_TOKEN = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV, } MOCK_SPEAKER_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, + CONF_DEVICE_CLASS: MediaPlayerDeviceClass.SPEAKER, } MOCK_INCLUDE_APPS = { @@ -199,6 +198,7 @@ def __init__(self, auth_token: str) -> None: MOCK_ZEROCONF_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( host=ZEROCONF_HOST, + addresses=[ZEROCONF_HOST], hostname="mock_hostname", name=ZEROCONF_NAME, port=ZEROCONF_PORT, diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 817f23d52c53e..3250163ef8e7f 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV +from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( CONF_APPS, @@ -77,7 +77,7 @@ async def test_user_flow_minimum_fields( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.SPEAKER async def test_user_flow_all_fields( @@ -102,7 +102,7 @@ async def test_user_flow_all_fields( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN assert CONF_APPS not in result["data"] @@ -339,7 +339,7 @@ async def test_user_tv_pairing_no_apps( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV assert CONF_APPS not in result["data"] @@ -412,7 +412,7 @@ async def test_import_flow_minimum_fields( DOMAIN, context={"source": SOURCE_IMPORT}, data=vol.Schema(VIZIO_SCHEMA)( - {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER} + {CONF_HOST: HOST, CONF_DEVICE_CLASS: MediaPlayerDeviceClass.SPEAKER} ), ) @@ -420,7 +420,7 @@ async def test_import_flow_minimum_fields( assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.SPEAKER assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP @@ -440,7 +440,7 @@ async def test_import_flow_all_fields( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP @@ -599,7 +599,7 @@ async def test_import_needs_pairing( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV async def test_import_with_apps_needs_pairing( @@ -641,7 +641,7 @@ async def test_import_with_apps_needs_pairing( assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] @@ -756,7 +756,7 @@ async def test_zeroconf_flow( assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + assert result["data"][CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.SPEAKER async def test_zeroconf_flow_already_configured( diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 80f722809511e..1c6377339276d 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -24,8 +24,6 @@ ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, - DEVICE_CLASS_SPEAKER, - DEVICE_CLASS_TV, DOMAIN as MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, @@ -37,6 +35,7 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + MediaPlayerDeviceClass, ) from homeassistant.components.media_player.const import ATTR_INPUT_SOURCE_LIST from homeassistant.components.vizio import validate_apps @@ -158,7 +157,9 @@ async def _test_setup_tv(hass: HomeAssistant, vizio_power_state: bool | None) -> ): await _add_config_entry_to_hass(hass, config_entry) - attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, ha_power_state) + attr = _get_attr_and_assert_base_attr( + hass, MediaPlayerDeviceClass.TV, ha_power_state + ) if ha_power_state == STATE_ON: _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV) assert "sound_mode" not in attr @@ -192,7 +193,7 @@ async def _test_setup_speaker( await _add_config_entry_to_hass(hass, config_entry) attr = _get_attr_and_assert_base_attr( - hass, DEVICE_CLASS_SPEAKER, ha_power_state + hass, MediaPlayerDeviceClass.SPEAKER, ha_power_state ) if ha_power_state == STATE_ON: _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_SPEAKER) @@ -219,7 +220,9 @@ async def _cm_for_test_setup_tv_with_apps( ): await _add_config_entry_to_hass(hass, config_entry) - attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON) + attr = _get_attr_and_assert_base_attr( + hass, MediaPlayerDeviceClass.TV, STATE_ON + ) assert ( attr["volume_level"] == float(int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)) @@ -714,7 +717,7 @@ async def test_setup_tv_without_mute( ): await _add_config_entry_to_hass(hass, config_entry) - attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON) + attr = _get_attr_and_assert_base_attr(hass, MediaPlayerDeviceClass.TV, STATE_ON) _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV) assert "sound_mode" not in attr assert "is_volume_muted" not in attr @@ -763,6 +766,6 @@ async def test_vizio_update_with_apps_on_input( unique_id=UNIQUE_ID, ) await _add_config_entry_to_hass(hass, config_entry) - attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON) + attr = _get_attr_and_assert_base_attr(hass, MediaPlayerDeviceClass.TV, STATE_ON) # app ID should not be in the attributes assert "app_id" not in attr diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 424bbe5e065b3..4eab1868057eb 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_mock_service -from tests.components.tts.test_init import mutagen_mock # noqa: F401 +from tests.components.tts.conftest import mutagen_mock # noqa: F401 URL = "https://api.voicerss.org/" FORM_DATA = { diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 3eb254784d1c9..8a47155815c88 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -19,6 +19,7 @@ TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( host="1.1.1.1", + addresses=["1.1.1.1"], hostname="mock_hostname", name="mock_name", port=3000, diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 0467566635663..4d964d9f08ada 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -5,11 +5,20 @@ import pytest -@pytest.fixture(autouse=True) -def mock_wrc(): +@pytest.fixture(name="mock_wrc", autouse=True) +def mock_wrc_fixture(): """Mock out WazeRouteCalculator.""" - with patch("homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator"): - yield + with patch( + "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator" + ) as mock_wrc: + yield mock_wrc + + +@pytest.fixture(name="mock_update") +def mock_update_fixture(mock_wrc): + """Mock an update to the sensor.""" + obj = mock_wrc.return_value + obj.calc_all_routes_info.return_value = {"My route": (150, 300)} @pytest.fixture(name="validate_config_entry") @@ -20,17 +29,15 @@ def validate_config_entry_fixture(): ) as mock_wrc: obj = mock_wrc.return_value obj.calc_all_routes_info.return_value = None - yield + yield mock_wrc -@pytest.fixture(name="bypass_setup") -def bypass_setup_fixture(): - """Bypass entry setup.""" - with patch( - "homeassistant.components.waze_travel_time.async_setup_entry", - return_value=True, - ): - yield +@pytest.fixture(name="invalidate_config_entry") +def invalidate_config_entry_fixture(validate_config_entry): + """Return invalid config entry.""" + obj = validate_config_entry.return_value + obj.calc_all_routes_info.return_value = {} + obj.calc_all_routes_info.side_effect = WRCError("test") @pytest.fixture(name="bypass_platform_setup") @@ -43,23 +50,11 @@ def bypass_platform_setup_fixture(): yield -@pytest.fixture(name="mock_update") -def mock_update_fixture(): - """Mock an update to the sensor.""" +@pytest.fixture(name="bypass_setup") +def bypass_setup_fixture(): + """Bypass entry setup.""" with patch( - "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator.calc_all_routes_info", - return_value={"My route": (150, 300)}, + "homeassistant.components.waze_travel_time.async_setup_entry", + return_value=True, ): yield - - -@pytest.fixture(name="invalidate_config_entry") -def invalidate_config_entry_fixture(): - """Return invalid config entry.""" - with patch( - "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" - ) as mock_wrc: - obj = mock_wrc.return_value - obj.calc_all_routes_info.return_value = {} - obj.calc_all_routes_info.side_effect = WRCError("test") - yield diff --git a/tests/components/waze_travel_time/const.py b/tests/components/waze_travel_time/const.py new file mode 100644 index 0000000000000..f56e8c5892e21 --- /dev/null +++ b/tests/components/waze_travel_time/const.py @@ -0,0 +1,13 @@ +"""Constants for waze_travel_time tests.""" + +from homeassistant.components.waze_travel_time.const import ( + CONF_DESTINATION, + CONF_ORIGIN, +) +from homeassistant.const import CONF_REGION + +MOCK_CONFIG = { + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", +} diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 015850ba1b817..c4414cdbefd5d 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Waze Travel Time config flow.""" +import pytest + from homeassistant import config_entries, data_entry_flow from homeassistant.components.waze_travel_time.const import ( CONF_AVOID_FERRIES, @@ -16,10 +18,13 @@ ) from homeassistant.const import CONF_NAME, CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL +from .const import MOCK_CONFIG + from tests.common import MockConfigEntry -async def test_minimum_fields(hass, validate_config_entry, bypass_setup): +@pytest.mark.usefixtures("validate_config_entry") +async def test_minimum_fields(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -29,11 +34,7 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - }, + MOCK_CONFIG, ) await hass.async_block_till_done() @@ -47,16 +48,11 @@ async def test_minimum_fields(hass, validate_config_entry, bypass_setup): } -async def test_options(hass, validate_config_entry, mock_update): +async def test_options(hass): """Test options flow.""" - entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - }, + data=MOCK_CONFIG, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -105,7 +101,8 @@ async def test_options(hass, validate_config_entry, mock_update): } -async def test_import(hass, validate_config_entry, mock_update): +@pytest.mark.usefixtures("validate_config_entry") +async def test_import(hass): """Test import for config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -145,30 +142,8 @@ async def test_import(hass, validate_config_entry, mock_update): } -async def _setup_dupe_import(hass, mock_update): - """Set up dupe import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - CONF_AVOID_FERRIES: True, - CONF_AVOID_SUBSCRIPTION_ROADS: True, - CONF_AVOID_TOLL_ROADS: True, - CONF_EXCL_FILTER: "exclude", - CONF_INCL_FILTER: "include", - CONF_REALTIME: False, - CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, - CONF_VEHICLE_TYPE: "taxi", - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - - -async def test_dupe(hass, validate_config_entry, bypass_setup): +@pytest.mark.usefixtures("validate_config_entry") +async def test_dupe(hass): """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -178,11 +153,7 @@ async def test_dupe(hass, validate_config_entry, bypass_setup): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - }, + MOCK_CONFIG, ) await hass.async_block_till_done() @@ -197,18 +168,15 @@ async def test_dupe(hass, validate_config_entry, bypass_setup): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - }, + MOCK_CONFIG, ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -async def test_invalid_config_entry(hass, invalidate_config_entry): +@pytest.mark.usefixtures("invalidate_config_entry") +async def test_invalid_config_entry(hass): """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -217,11 +185,7 @@ async def test_invalid_config_entry(hass, invalidate_config_entry): assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_ORIGIN: "location1", - CONF_DESTINATION: "location2", - CONF_REGION: "US", - }, + MOCK_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py new file mode 100644 index 0000000000000..2b28190e43075 --- /dev/null +++ b/tests/components/waze_travel_time/test_sensor.py @@ -0,0 +1,124 @@ +"""Test Waze Travel Time sensors.""" + +from WazeRouteCalculator import WRCError +import pytest + +from homeassistant.components.waze_travel_time.const import ( + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + CONF_REALTIME, + CONF_UNITS, + CONF_VEHICLE_TYPE, + DOMAIN, +) +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL + +from .const import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_config") +async def mock_config_fixture(hass, data, options): + """Mock a Waze Travel Time config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options=options, + entry_id="test", + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +@pytest.fixture(name="mock_update_wrcerror") +def mock_update_wrcerror_fixture(mock_wrc): + """Mock an update to the sensor failed with WRCError.""" + obj = mock_wrc.return_value + obj.calc_all_routes_info.side_effect = WRCError("test") + yield + + +@pytest.fixture(name="mock_update_keyerror") +def mock_update_keyerror_fixture(mock_wrc): + """Mock an update to the sensor failed with KeyError.""" + obj = mock_wrc.return_value + obj.calc_all_routes_info.side_effect = KeyError("test") + yield + + +@pytest.mark.parametrize( + "data,options", + [(MOCK_CONFIG, {})], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_sensor(hass): + """Test that sensor works.""" + assert hass.states.get("sensor.waze_travel_time").state == "150" + assert ( + hass.states.get("sensor.waze_travel_time").attributes["attribution"] + == "Powered by Waze" + ) + assert hass.states.get("sensor.waze_travel_time").attributes["duration"] == 150 + assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 300 + assert hass.states.get("sensor.waze_travel_time").attributes["route"] == "My route" + assert ( + hass.states.get("sensor.waze_travel_time").attributes["origin"] == "location1" + ) + assert ( + hass.states.get("sensor.waze_travel_time").attributes["destination"] + == "location2" + ) + assert ( + hass.states.get("sensor.waze_travel_time").attributes["unit_of_measurement"] + == "min" + ) + assert hass.states.get("sensor.waze_travel_time").attributes["icon"] == "mdi:car" + + +@pytest.mark.parametrize( + "data,options", + [ + ( + MOCK_CONFIG, + { + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_REALTIME: True, + CONF_VEHICLE_TYPE: "car", + CONF_AVOID_TOLL_ROADS: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_FERRIES: True, + }, + ) + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_imperial(hass): + """Test that the imperial option works.""" + assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 186.4113 + + +@pytest.mark.usefixtures("mock_update_wrcerror") +async def test_sensor_failed_wrcerror(hass, caplog): + """Test that sensor update fails with log message.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.waze_travel_time").state == "unknown" + assert "Error on retrieving data: " in caplog.text + + +@pytest.mark.usefixtures("mock_update_keyerror") +async def test_sensor_failed_keyerror(hass, caplog): + """Test that sensor update fails with log message.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.waze_travel_time").state == "unknown" + assert "Error retrieving data from server" in caplog.text diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index c249b491d9a8f..f0ebdd70e97c7 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -59,6 +59,7 @@ STATE_OFF, STATE_ON, ) +from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -67,7 +68,7 @@ from . import setup_webostv from .const import CHANNEL_2, ENTITY_ID, TV_NAME -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache @pytest.mark.parametrize( @@ -556,3 +557,132 @@ async def test_supported_features(hass, client, monkeypatch): attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + +async def test_cached_supported_features(hass, client, monkeypatch): + """Test test supported features.""" + monkeypatch.setattr(client, "is_on", False) + monkeypatch.setattr(client, "sound_output", None) + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_TURN_ON + mock_restore_cache( + hass, + [ + State( + ENTITY_ID, + STATE_OFF, + attributes={ + ATTR_SUPPORTED_FEATURES: supported, + }, + ) + ], + ) + await setup_webostv(hass) + await client.mock_state_update() + + # TV off, restored state supports mute, step + # validate SUPPORT_TURN_ON is not cached + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported & ~SUPPORT_TURN_ON + + # TV on, support volume mute, step + monkeypatch.setattr(client, "is_on", True) + monkeypatch.setattr(client, "sound_output", "external_speaker") + await client.mock_state_update() + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + # TV off, support volume mute, step + monkeypatch.setattr(client, "is_on", False) + monkeypatch.setattr(client, "sound_output", None) + await client.mock_state_update() + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + # TV on, support volume mute, step, set + monkeypatch.setattr(client, "is_on", True) + monkeypatch.setattr(client, "sound_output", "speaker") + await client.mock_state_update() + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + # TV off, support volume mute, step, set + monkeypatch.setattr(client, "is_on", False) + monkeypatch.setattr(client, "sound_output", None) + await client.mock_state_update() + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + # Test support turn on is updated on cached state + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "webostv.turn_on", + "entity_id": ENTITY_ID, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ENTITY_ID, + "id": "{{ trigger.id }}", + }, + }, + }, + ], + }, + ) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported | SUPPORT_TURN_ON + + +async def test_supported_features_no_cache(hass, client, monkeypatch): + """Test supported features if device is off and no cache.""" + monkeypatch.setattr(client, "is_on", False) + monkeypatch.setattr(client, "sound_output", None) + await setup_webostv(hass) + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + +async def test_supported_features_ignore_cache(hass, client): + """Test ignore cached supported features if device is on at startup.""" + mock_restore_cache( + hass, + [ + State( + ENTITY_ID, + STATE_OFF, + attributes={ + ATTR_SUPPORTED_FEATURES: SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME, + }, + ) + ], + ) + await setup_webostv(hass) + + supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | SUPPORT_VOLUME_SET + attrs = hass.states.get(ENTITY_ID).attributes + + assert attrs[ATTR_SUPPORTED_FEATURES] == supported diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index 7e150c6eb7828..92fb151c1b34e 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -4,9 +4,13 @@ from aiowebostv import WebOsTvPairError import pytest -from homeassistant.components.notify import ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_MESSAGE, + DOMAIN as NOTIFY_DOMAIN, +) from homeassistant.components.webostv import DOMAIN -from homeassistant.const import CONF_ICON, CONF_SERVICE_DATA +from homeassistant.const import ATTR_ICON from homeassistant.setup import async_setup_component from . import setup_webostv @@ -26,8 +30,8 @@ async def test_notify(hass, client): TV_NAME, { ATTR_MESSAGE: MESSAGE, - CONF_SERVICE_DATA: { - CONF_ICON: ICON_PATH, + ATTR_DATA: { + ATTR_ICON: ICON_PATH, }, }, blocking=True, @@ -41,7 +45,7 @@ async def test_notify(hass, client): TV_NAME, { ATTR_MESSAGE: MESSAGE, - CONF_SERVICE_DATA: { + ATTR_DATA: { "OTHER_DATA": "not_used", }, }, @@ -51,6 +55,20 @@ async def test_notify(hass, client): assert client.connect.call_count == 1 client.send_message.assert_called_with(MESSAGE, icon_path=None) + await hass.services.async_call( + NOTIFY_DOMAIN, + TV_NAME, + { + ATTR_MESSAGE: "only message, no data", + }, + blocking=True, + ) + + assert client.connect.call_count == 1 + assert client.send_message.call_args == call( + "only message, no data", icon_path=None + ) + async def test_notify_not_connected(hass, client, monkeypatch): """Test sending a message when client is not connected.""" @@ -63,8 +81,8 @@ async def test_notify_not_connected(hass, client, monkeypatch): TV_NAME, { ATTR_MESSAGE: MESSAGE, - CONF_SERVICE_DATA: { - CONF_ICON: ICON_PATH, + ATTR_DATA: { + ATTR_ICON: ICON_PATH, }, }, blocking=True, @@ -85,8 +103,8 @@ async def test_icon_not_found(hass, caplog, client, monkeypatch): TV_NAME, { ATTR_MESSAGE: MESSAGE, - CONF_SERVICE_DATA: { - CONF_ICON: ICON_PATH, + ATTR_DATA: { + ATTR_ICON: ICON_PATH, }, }, blocking=True, @@ -116,8 +134,8 @@ async def test_connection_errors(hass, caplog, client, monkeypatch, side_effect, TV_NAME, { ATTR_MESSAGE: MESSAGE, - CONF_SERVICE_DATA: { - CONF_ICON: ICON_PATH, + ATTR_DATA: { + ATTR_ICON: ICON_PATH, }, }, blocking=True, diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 58c9b414d5a16..742d9bddd3884 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -587,13 +587,20 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): """Test get_states command not allows NaN floats.""" - hass.states.async_set("greeting.hello", "world", {"hello": float("NaN")}) + hass.states.async_set("greeting.hello", "world") + hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) + hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) msg = await websocket_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_UNKNOWN_ERROR + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == [ + hass.states.get("greeting.hello").as_dict(), + hass.states.get("greeting.bye").as_dict(), + ] async def test_subscribe_unsubscribe_events_whitelist( @@ -706,6 +713,38 @@ async def test_render_template_renders_template(hass, websocket_client): } +async def test_render_template_with_timeout_and_variables(hass, websocket_client): + """Test a template with a timeout and variables renders without error.""" + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "timeout": 10, + "variables": {"test": {"value": "hello"}}, + "template": "{{ test.value }}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == { + "result": "hello", + "listeners": { + "all": False, + "domains": [], + "entities": [], + "time": False, + }, + } + + async def test_render_template_manual_entity_ids_no_longer_needed( hass, websocket_client ): @@ -1254,3 +1293,60 @@ async def test_integration_setup_info(hass, websocket_client, hass_admin_user): {"domain": "august", "seconds": 12.5}, {"domain": "isy994", "seconds": 12.8}, ] + + +@pytest.mark.parametrize( + "key,config", + ( + ("trigger", {"platform": "event", "event_type": "hello"}), + ( + "condition", + {"condition": "state", "entity_id": "hello.world", "state": "paulus"}, + ), + ("action", {"service": "domain_test.test_service"}), + ), +) +async def test_validate_config_works(websocket_client, key, config): + """Test config validation.""" + await websocket_client.send_json({"id": 7, "type": "validate_config", key: config}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == {key: {"valid": True, "error": None}} + + +@pytest.mark.parametrize( + "key,config,error", + ( + ( + "trigger", + {"platform": "non_existing", "event_type": "hello"}, + "Invalid platform 'non_existing' specified", + ), + ( + "condition", + { + "condition": "non_existing", + "entity_id": "hello.world", + "state": "paulus", + }, + "Unexpected value for condition: 'non_existing'. Expected and, device, not, numeric_state, or, state, sun, template, time, trigger, zone", + ), + ( + "action", + {"non_existing": "domain_test.test_service"}, + "Unable to determine action @ data[0]", + ), + ), +) +async def test_validate_config_invalid(websocket_client, key, config, error): + """Test config validation.""" + await websocket_client.send_json({"id": 7, "type": "validate_config", key: config}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == {key: {"valid": False, "error": error}} diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 336c79d22b8c2..c3564d2b21b3f 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -76,7 +76,8 @@ async def test_non_json_message(hass, websocket_client, caplog): msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] + assert msg["success"] + assert msg["result"] == [] assert ( f"Unable to serialize to JSON. Bad data found at $.result[0](State: test_domain.entity).attributes.bad={bad_data}(" in caplog.text diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 3ec156e69493b..618879f4b7fec 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -83,13 +83,13 @@ async def test_message_to_json(caplog): json_str = message_to_json({"id": 1, "message": "xyz"}) - assert json_str == '{"id": 1, "message": "xyz"}' + assert json_str == '{"id":1,"message":"xyz"}' json_str2 = message_to_json({"id": 1, "message": _Unserializeable()}) assert ( json_str2 - == '{"id": 1, "type": "result", "success": false, "error": {"code": "unknown_error", "message": "Invalid JSON in response"}}' + == '{"id":1,"type":"result","success":false,"error":{"code":"unknown_error","message":"Invalid JSON in response"}}' ) assert "Unable to serialize to JSON" in caplog.text diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py new file mode 100644 index 0000000000000..c4a31b0a39410 --- /dev/null +++ b/tests/components/wiz/__init__.py @@ -0,0 +1,267 @@ +"""Tests for the WiZ Platform integration.""" + +from contextlib import contextmanager +from copy import deepcopy +import json +from typing import Callable +from unittest.mock import AsyncMock, MagicMock, patch + +from pywizlight import SCENES, BulbType, PilotParser, wizlight +from pywizlight.bulblibrary import BulbClass, Features, KelvinRange +from pywizlight.discovery import DiscoveredBulb + +from homeassistant.components.wiz.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +FAKE_STATE = PilotParser( + { + "mac": "a8bb50818e7c", + "rssi": -55, + "src": "hb", + "mqttCd": 0, + "ts": 1644425347, + "state": True, + "sceneId": 0, + "r": 0, + "g": 0, + "b": 255, + "c": 0, + "w": 0, + "dimming": 100, + } +) +FAKE_IP = "1.1.1.1" +FAKE_MAC = "ABCABCABCABC" +FAKE_BULB_CONFIG = { + "method": "getSystemConfig", + "env": "pro", + "result": { + "mac": FAKE_MAC, + "homeId": 653906, + "roomId": 989983, + "moduleName": "ESP_0711_STR", + "fwVersion": "1.21.0", + "groupId": 0, + "drvConf": [20, 2], + "ewf": [255, 0, 255, 255, 0, 0, 0], + "ewfHex": "ff00ffff000000", + "ping": 0, + }, +} +FAKE_SOCKET_CONFIG = deepcopy(FAKE_BULB_CONFIG) +FAKE_SOCKET_CONFIG["result"]["moduleName"] = "ESP10_SOCKET_06" +FAKE_EXTENDED_WHITE_RANGE = [2200, 2700, 6500, 6500] +TEST_SYSTEM_INFO = {"id": FAKE_MAC, "name": "Test Bulb"} +TEST_CONNECTION = {CONF_HOST: "1.1.1.1"} +TEST_NO_IP = {CONF_HOST: "this is no IP input"} + +FAKE_BULB_CONFIG = json.loads( + '{"method":"getSystemConfig","env":"pro","result":\ + {"mac":"ABCABCABCABC",\ + "homeId":653906,\ + "roomId":989983,\ + "moduleName":"ESP_0711_STR",\ + "fwVersion":"1.21.0",\ + "groupId":0,"drvConf":[20,2],\ + "ewf":[255,0,255,255,0,0,0],\ + "ewfHex":"ff00ffff000000",\ + "ping":0}}' +) + +REAL_BULB_CONFIG = json.loads( + '{"method":"getSystemConfig","env":"pro","result":\ + {"mac":"ABCABCABCABC",\ + "homeId":653906,\ + "roomId":989983,\ + "moduleName":"ESP01_SHRGB_03",\ + "fwVersion":"1.21.0",\ + "groupId":0,"drvConf":[20,2],\ + "ewf":[255,0,255,255,0,0,0],\ + "ewfHex":"ff00ffff000000",\ + "ping":0}}' +) +FAKE_DUAL_HEAD_RGBWW_BULB = BulbType( + bulb_type=BulbClass.RGB, + name="ESP01_DHRGB_03", + features=Features( + color=True, color_tmp=True, effect=True, brightness=True, dual_head=True + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=2, + white_to_color_ratio=80, +) +FAKE_RGBWW_BULB = BulbType( + bulb_type=BulbClass.RGB, + name="ESP01_SHRGB_03", + features=Features( + color=True, color_tmp=True, effect=True, brightness=True, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=2, + white_to_color_ratio=80, +) +FAKE_RGBW_BULB = BulbType( + bulb_type=BulbClass.RGB, + name="ESP01_SHRGB_03", + features=Features( + color=True, color_tmp=True, effect=True, brightness=True, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=1, + white_to_color_ratio=80, +) +FAKE_DIMMABLE_BULB = BulbType( + bulb_type=BulbClass.DW, + name="ESP01_DW_03", + features=Features( + color=False, color_tmp=False, effect=True, brightness=True, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=1, + white_to_color_ratio=80, +) +FAKE_TURNABLE_BULB = BulbType( + bulb_type=BulbClass.TW, + name="ESP01_TW_03", + features=Features( + color=False, color_tmp=True, effect=True, brightness=True, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=1, + white_to_color_ratio=80, +) +FAKE_SOCKET = BulbType( + bulb_type=BulbClass.SOCKET, + name="ESP01_SOCKET_03", + features=Features( + color=False, color_tmp=False, effect=False, brightness=False, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.0.0", + white_channels=2, + white_to_color_ratio=80, +) +FAKE_OLD_FIRMWARE_DIMMABLE_BULB = BulbType( + bulb_type=BulbClass.DW, + name=None, + features=Features( + color=False, color_tmp=False, effect=True, brightness=True, dual_head=False + ), + kelvin_range=None, + fw_version="1.8.0", + white_channels=1, + white_to_color_ratio=80, +) + + +async def setup_integration( + hass: HomeAssistantType, +) -> MockConfigEntry: + """Mock ConfigEntry in Home Assistant.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SYSTEM_INFO["id"], + data={ + CONF_HOST: "127.0.0.1", + CONF_NAME: TEST_SYSTEM_INFO["name"], + }, + ) + + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +def _mocked_wizlight(device, extended_white_range, bulb_type) -> wizlight: + bulb = MagicMock(auto_spec=wizlight, name="Mocked wizlight") + + async def _save_setup_callback(callback: Callable) -> None: + bulb.push_callback = callback + + bulb.getBulbConfig = AsyncMock(return_value=device or FAKE_BULB_CONFIG) + bulb.getExtendedWhiteRange = AsyncMock( + return_value=extended_white_range or FAKE_EXTENDED_WHITE_RANGE + ) + bulb.getMac = AsyncMock(return_value=FAKE_MAC) + bulb.turn_on = AsyncMock() + bulb.turn_off = AsyncMock() + bulb.updateState = AsyncMock(return_value=FAKE_STATE) + bulb.getSupportedScenes = AsyncMock(return_value=list(SCENES)) + bulb.start_push = AsyncMock(side_effect=_save_setup_callback) + bulb.async_close = AsyncMock() + bulb.set_speed = AsyncMock() + bulb.set_ratio = AsyncMock() + bulb.diagnostics = { + "mocked": "mocked", + "roomId": 123, + "homeId": 34, + } + bulb.state = FAKE_STATE + bulb.mac = FAKE_MAC + bulb.bulbtype = bulb_type or FAKE_DIMMABLE_BULB + bulb.get_bulbtype = AsyncMock(return_value=bulb_type or FAKE_DIMMABLE_BULB) + + return bulb + + +def _patch_wizlight(device=None, extended_white_range=None, bulb_type=None): + @contextmanager + def _patcher(): + bulb = device or _mocked_wizlight(device, extended_white_range, bulb_type) + with patch("homeassistant.components.wiz.wizlight", return_value=bulb), patch( + "homeassistant.components.wiz.config_flow.wizlight", + return_value=bulb, + ): + yield + + return _patcher() + + +def _patch_discovery(): + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.wiz.discovery.find_wizlights", + return_value=[DiscoveredBulb(FAKE_IP, FAKE_MAC)], + ): + yield + + return _patcher() + + +async def async_setup_integration( + hass, wizlight=None, device=None, extended_white_range=None, bulb_type=None +): + """Set up the integration with a mock device.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FAKE_MAC, + data={CONF_HOST: FAKE_IP}, + ) + entry.add_to_hass(hass) + bulb = wizlight or _mocked_wizlight(device, extended_white_range, bulb_type) + with _patch_discovery(), _patch_wizlight(device=bulb): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return bulb, entry + + +async def async_push_update(hass, device, params): + """Push an update to the device.""" + device.state = PilotParser(params) + device.status = params.get("state") + device.push_callback(device.state) + await hass.async_block_till_done() diff --git a/tests/components/wiz/test_binary_sensor.py b/tests/components/wiz/test_binary_sensor.py new file mode 100644 index 0000000000000..adfef066e16ee --- /dev/null +++ b/tests/components/wiz/test_binary_sensor.py @@ -0,0 +1,83 @@ +"""Tests for WiZ binary_sensor platform.""" + +from homeassistant import config_entries +from homeassistant.components import wiz +from homeassistant.components.wiz.binary_sensor import OCCUPANCY_UNIQUE_ID +from homeassistant.const import CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + FAKE_IP, + FAKE_MAC, + _mocked_wizlight, + _patch_discovery, + _patch_wizlight, + async_push_update, + async_setup_integration, +) + +from tests.common import MockConfigEntry + + +async def test_binary_sensor_created_from_push_updates(hass: HomeAssistant) -> None: + """Test a binary sensor created from push updates.""" + bulb, _ = await async_setup_integration(hass) + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": True}) + + entity_id = "binary_sensor.mock_title_occupancy" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_occupancy" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": False}) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +async def test_binary_sensor_restored_from_registry(hass: HomeAssistant) -> None: + """Test a binary sensor restored from registry with state unknown.""" + entry = MockConfigEntry( + domain=wiz.DOMAIN, + unique_id=FAKE_MAC, + data={CONF_HOST: FAKE_IP}, + ) + entry.add_to_hass(hass) + bulb = _mocked_wizlight(None, None, None) + + entity_registry = er.async_get(hass) + reg_ent = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, wiz.DOMAIN, OCCUPANCY_UNIQUE_ID.format(bulb.mac) + ) + entity_id = reg_ent.entity_id + + with _patch_discovery(), _patch_wizlight(device=bulb): + await async_setup_component(hass, wiz.DOMAIN, {wiz.DOMAIN: {}}) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": True}) + + assert entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_occupancy" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + + +async def test_binary_sensor_never_created_no_error_on_unload( + hass: HomeAssistant, +) -> None: + """Test a binary sensor does not error on unload.""" + _, entry = await async_setup_integration(hass) + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py new file mode 100644 index 0000000000000..f8426ece56db7 --- /dev/null +++ b/tests/components/wiz/test_config_flow.py @@ -0,0 +1,477 @@ +"""Test the WiZ Platform config flow.""" +from unittest.mock import patch + +import pytest +from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.wiz.config_flow import CONF_DEVICE +from homeassistant.components.wiz.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from . import ( + FAKE_DIMMABLE_BULB, + FAKE_EXTENDED_WHITE_RANGE, + FAKE_IP, + FAKE_MAC, + FAKE_RGBW_BULB, + FAKE_RGBWW_BULB, + FAKE_SOCKET, + TEST_CONNECTION, + TEST_SYSTEM_INFO, + _patch_discovery, + _patch_wizlight, +) + +from tests.common import MockConfigEntry + +DHCP_DISCOVERY = dhcp.DhcpServiceInfo( + hostname="wiz_abcabc", + ip=FAKE_IP, + macaddress=FAKE_MAC, +) + + +INTEGRATION_DISCOVERY = { + "ip_address": FAKE_IP, + "mac_address": FAKE_MAC, +} + + +async def test_form(hass): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + # Patch functions + with _patch_wizlight(), patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_CONNECTION, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "WiZ Dimmable White ABCABC" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_flow_enters_dns_name(hass): + """Test we reject dns names and want ips.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "ip.only"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_ip"} + + with _patch_wizlight(), patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + TEST_CONNECTION, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "WiZ Dimmable White ABCABC" + assert result3["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect, error_base", + [ + (WizLightTimeOutError, "bulb_time_out"), + (WizLightConnectionError, "no_wiz_light"), + (Exception, "unknown"), + (ConnectionRefusedError, "cannot_connect"), + ], +) +async def test_user_form_exceptions(hass, side_effect, error_base): + """Test all user exceptions in the flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wiz.wizlight.getBulbConfig", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_CONNECTION, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": error_base} + + +async def test_form_updates_unique_id(hass): + """Test a duplicate id aborts and updates existing entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SYSTEM_INFO["id"], + data={CONF_HOST: "dummy"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with _patch_wizlight(): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_CONNECTION, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + assert entry.data[CONF_HOST] == FAKE_IP + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, INTEGRATION_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_connection_fails(hass, source, data): + """Test we abort on connection failure.""" + with patch( + "homeassistant.components.wiz.wizlight.getBulbConfig", + side_effect=WizLightTimeOutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +@pytest.mark.parametrize( + "source, data, bulb_type, extended_white_range, name", + [ + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY, + FAKE_DIMMABLE_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ Dimmable White ABCABC", + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + INTEGRATION_DISCOVERY, + FAKE_DIMMABLE_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ Dimmable White ABCABC", + ), + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY, + FAKE_RGBW_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ RGBW Tunable ABCABC", + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + INTEGRATION_DISCOVERY, + FAKE_RGBW_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ RGBW Tunable ABCABC", + ), + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY, + FAKE_RGBWW_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ RGBWW Tunable ABCABC", + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + INTEGRATION_DISCOVERY, + FAKE_RGBWW_BULB, + FAKE_EXTENDED_WHITE_RANGE, + "WiZ RGBWW Tunable ABCABC", + ), + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY, + FAKE_SOCKET, + None, + "WiZ Socket ABCABC", + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + INTEGRATION_DISCOVERY, + FAKE_SOCKET, + None, + "WiZ Socket ABCABC", + ), + ], +) +async def test_discovered_by_dhcp_or_integration_discovery( + hass, source, data, bulb_type, extended_white_range, name +): + """Test we can configure when discovered from dhcp or discovery.""" + with _patch_wizlight( + device=None, extended_white_range=extended_white_range, bulb_type=bulb_type + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + with _patch_wizlight( + device=None, extended_white_range=extended_white_range, bulb_type=bulb_type + ), patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == name + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, INTEGRATION_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_integration_discovery_updates_host( + hass, source, data +): + """Test dhcp or discovery updates existing host.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_SYSTEM_INFO["id"], + data={CONF_HOST: "dummy"}, + ) + entry.add_to_hass(hass) + + with _patch_wizlight(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == FAKE_IP + + +async def test_setup_via_discovery(hass): + """Test setting up via discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # test we can try again + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_wizlight(), patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.wiz.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: FAKE_MAC}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "WiZ Dimmable White ABCABC" + assert result3["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_setup_via_discovery_cannot_connect(hass): + """Test setting up via discovery and we fail to connect to the discovered device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with patch( + "homeassistant.components.wiz.wizlight.getBulbConfig", + side_effect=WizLightTimeOutError, + ), _patch_discovery(): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: FAKE_MAC}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "cannot_connect" + + +async def test_setup_via_discovery_exception_finds_nothing(hass): + """Test we do not find anything if discovery throws.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with patch( + "homeassistant.components.wiz.discovery.find_wizlights", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_with_firmware_update(hass): + """Test we check the device again between first discovery and config entry creation.""" + with _patch_wizlight( + device=None, + extended_white_range=FAKE_EXTENDED_WHITE_RANGE, + bulb_type=FAKE_RGBW_BULB, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=INTEGRATION_DISCOVERY, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + # In between discovery and when the user clicks to set it up the firmware + # updates and we now can see its really RGBWW not RGBW since the older + # firmwares did not tell us how many white channels exist + + with patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, _patch_wizlight( + device=None, + extended_white_range=FAKE_EXTENDED_WHITE_RANGE, + bulb_type=FAKE_RGBWW_BULB, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "WiZ RGBWW Tunable ABCABC" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/wiz/test_diagnostics.py b/tests/components/wiz/test_diagnostics.py new file mode 100644 index 0000000000000..c993072bc073e --- /dev/null +++ b/tests/components/wiz/test_diagnostics.py @@ -0,0 +1,19 @@ +"""Test WiZ diagnostics.""" +from . import async_setup_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + _, entry = await async_setup_integration(hass) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == { + "data": { + "homeId": "**REDACTED**", + "mocked": "mocked", + "roomId": "**REDACTED**", + }, + "entry": {"data": {"host": "1.1.1.1"}, "title": "Mock Title"}, + } diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py new file mode 100644 index 0000000000000..58afb5c944a8f --- /dev/null +++ b/tests/components/wiz/test_init.py @@ -0,0 +1,60 @@ +"""Tests for wiz integration.""" +import datetime +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from . import ( + FAKE_MAC, + FAKE_SOCKET, + _mocked_wizlight, + _patch_discovery, + _patch_wizlight, + async_setup_integration, +) + +from tests.common import async_fire_time_changed + + +async def test_setup_retry(hass: HomeAssistant) -> None: + """Test setup is retried on error.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.getMac = AsyncMock(side_effect=OSError) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + bulb.getMac = AsyncMock(return_value=FAKE_MAC) + + with _patch_discovery(), _patch_wizlight(device=bulb): + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) + await hass.async_block_till_done() + assert entry.state == config_entries.ConfigEntryState.LOADED + + +async def test_cleanup_on_shutdown(hass: HomeAssistant) -> None: + """Test the socket is cleaned up on shutdown.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.LOADED + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + bulb.async_close.assert_called_once() + + +async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: + """Test the socket is cleaned up on failed first update.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.updateState = AsyncMock(side_effect=OSError) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + bulb.async_close.assert_called_once() + + +async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: + """Test setup is retried when the wrong device is found.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.mac = "dddddddddddd" + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY diff --git a/tests/components/wiz/test_light.py b/tests/components/wiz/test_light.py new file mode 100644 index 0000000000000..48166e941d4f1 --- /dev/null +++ b/tests/components/wiz/test_light.py @@ -0,0 +1,203 @@ +"""Tests for light platform.""" + +from pywizlight import PilotBuilder + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + FAKE_MAC, + FAKE_OLD_FIRMWARE_DIMMABLE_BULB, + FAKE_RGBW_BULB, + FAKE_RGBWW_BULB, + FAKE_TURNABLE_BULB, + async_push_update, + async_setup_integration, +) + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass) + entity_id = "light.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_light_operation(hass: HomeAssistant) -> None: + """Test a light operation.""" + bulb, _ = await async_setup_integration(hass) + entity_id = "light.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_off.assert_called_once() + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "state": False}) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_on.assert_called_once() + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "state": True}) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rgbww_light(hass: HomeAssistant) -> None: + """Test a light operation with a rgbww light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_RGBWW_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (1, 2, 3, 4, 5)}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"b": 3, "c": 4, "g": 2, "r": 1, "state": True, "w": 5} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_RGBWW_COLOR] == (1, 2, 3, 4, 5) + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_TEMP] == 153 + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Ocean"}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"sceneId": 1, "state": True} + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "Ocean" + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Rhythm"}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"state": True} + + +async def test_rgbw_light(hass: HomeAssistant) -> None: + """Test a light operation with a rgbww light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_RGBW_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (1, 2, 3, 4)}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"b": 3, "g": 2, "r": 1, "state": True, "w": 4} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_RGBW_COLOR] == (1, 2, 3, 4) + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + + +async def test_turnable_light(hass: HomeAssistant) -> None: + """Test a light operation with a turnable light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_TURNABLE_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_TEMP] == 153 + + +async def test_old_firmware_dimmable_light(hass: HomeAssistant) -> None: + """Test a light operation with a dimmable light with old firmware.""" + bulb, _ = await async_setup_integration( + hass, bulb_type=FAKE_OLD_FIRMWARE_DIMMABLE_BULB + ) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "state": True} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 100, "state": True} diff --git a/tests/components/wiz/test_number.py b/tests/components/wiz/test_number.py new file mode 100644 index 0000000000000..1d45be9b8cfdc --- /dev/null +++ b/tests/components/wiz/test_number.py @@ -0,0 +1,62 @@ +"""Tests for the number platform.""" + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + FAKE_DUAL_HEAD_RGBWW_BULB, + FAKE_MAC, + async_push_update, + async_setup_integration, +) + + +async def test_speed_operation(hass: HomeAssistant) -> None: + """Test changing a speed.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_DUAL_HEAD_RGBWW_BULB) + await async_push_update(hass, bulb, {"mac": FAKE_MAC}) + entity_id = "number.mock_title_effect_speed" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_effect_speed" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "speed": 50}) + assert hass.states.get(entity_id).state == "50.0" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 30}, + blocking=True, + ) + bulb.set_speed.assert_called_with(30) + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "speed": 30}) + assert hass.states.get(entity_id).state == "30.0" + + +async def test_ratio_operation(hass: HomeAssistant) -> None: + """Test changing a dual head ratio.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_DUAL_HEAD_RGBWW_BULB) + await async_push_update(hass, bulb, {"mac": FAKE_MAC}) + entity_id = "number.mock_title_dual_head_ratio" + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_dual_head_ratio" + ) + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "ratio": 50}) + assert hass.states.get(entity_id).state == "50.0" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 30}, + blocking=True, + ) + bulb.set_ratio.assert_called_with(30) + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "ratio": 30}) + assert hass.states.get(entity_id).state == "30.0" diff --git a/tests/components/wiz/test_switch.py b/tests/components/wiz/test_switch.py new file mode 100644 index 0000000000000..e728ff4a645e5 --- /dev/null +++ b/tests/components/wiz/test_switch.py @@ -0,0 +1,66 @@ +"""Tests for switch platform.""" + +import datetime + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import FAKE_MAC, FAKE_SOCKET, async_push_update, async_setup_integration + +from tests.common import async_fire_time_changed + + +async def test_switch_operation(hass: HomeAssistant) -> None: + """Test switch operation.""" + switch, _ = await async_setup_integration(hass, bulb_type=FAKE_SOCKET) + entity_id = "switch.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_off.assert_called_once() + + await async_push_update(hass, switch, {"mac": FAKE_MAC, "state": False}) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_on.assert_called_once() + + await async_push_update(hass, switch, {"mac": FAKE_MAC, "state": True}) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_update_fails(hass: HomeAssistant) -> None: + """Test switch update fails when push updates are not working.""" + switch, _ = await async_setup_integration(hass, bulb_type=FAKE_SOCKET) + entity_id = "switch.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_off.assert_called_once() + + switch.updateState.side_effect = OSError + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(seconds=15)) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 2702370840094..c23f35534b831 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -51,6 +51,7 @@ async def test_full_zeroconf_flow_implementation( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -110,6 +111,7 @@ async def test_zeroconf_connection_error( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -168,6 +170,7 @@ async def test_zeroconf_without_mac_device_exists_abort( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -192,6 +195,7 @@ async def test_zeroconf_with_mac_device_exists_abort( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, @@ -216,6 +220,7 @@ async def test_zeroconf_with_cct_channel_abort( context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host="192.168.1.123", + addresses=["192.168.1.123"], hostname="example.local.", name="mock_name", port=None, diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 554e0460443df..c27f273f26b5f 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -403,6 +403,7 @@ async def test_zeroconf_success(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=TEST_ZEROCONF_NAME, port=None, @@ -449,6 +450,7 @@ async def test_zeroconf_missing_data(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=TEST_ZEROCONF_NAME, port=None, @@ -468,6 +470,7 @@ async def test_zeroconf_unknown_device(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name="not-a-xiaomi-aqara-gateway", port=None, diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 3be52e8323742..09f0b4c0fb63e 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -394,6 +394,7 @@ async def test_zeroconf_gateway_success(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=TEST_ZEROCONF_NAME, port=None, @@ -436,6 +437,7 @@ async def test_zeroconf_unknown_device(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name="not-a-xiaomi-miio-device", port=None, @@ -455,6 +457,7 @@ async def test_zeroconf_no_data(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=None, + addresses=[], hostname="mock_hostname", name=None, port=None, @@ -474,6 +477,7 @@ async def test_zeroconf_missing_data(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=TEST_ZEROCONF_NAME, port=None, @@ -771,6 +775,7 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test): context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( host=TEST_HOST, + addresses=[TEST_HOST], hostname="mock_hostname", name=zeroconf_name_to_test, port=None, diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index a7c454851bf6c..fee5e5ab97ab9 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -38,7 +38,6 @@ async def test_form(hass: HomeAssistant) -> None: { "username": "test-username", "password": "test-password", - "name": "Yale Smart Alarm", "area_id": "1", }, ) @@ -81,7 +80,6 @@ async def test_form_invalid_auth( { "username": "test-username", "password": "test-password", - "name": "Yale Smart Alarm", "area_id": "1", }, ) @@ -101,7 +99,6 @@ async def test_form_invalid_auth( { "username": "test-username", "password": "test-password", - "name": "Yale Smart Alarm", "area_id": "1", }, ) @@ -124,7 +121,6 @@ async def test_form_invalid_auth( { "username": "test-username", "password": "test-password", - "name": "Yale Smart Alarm", "area_id": "1", }, { diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index db4dbce4b8b8d..495009eecf91e 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -14,7 +14,7 @@ from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_mock_service -from tests.components.tts.test_init import ( # noqa: F401, pylint: disable=unused-import +from tests.components.tts.conftest import ( # noqa: F401, pylint: disable=unused-import mutagen_mock, ) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index b48cfc5402a89..d0112c5a5444f 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -42,6 +42,7 @@ ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + addresses=[IP_ADDRESS], port=54321, hostname=f"yeelink-light-strip1_miio{ID_DECIMAL}.local.", type="_miio._udp.local.", @@ -153,7 +154,7 @@ def _mocked_bulb(cannot_connect=False): bulb.async_set_power_mode = AsyncMock() bulb.async_set_scene = AsyncMock() bulb.async_set_default = AsyncMock() - bulb.start_music = MagicMock() + bulb.async_start_music = AsyncMock() return bulb diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index aa5e7f98a4551..205b5ddfa611d 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -467,6 +467,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): context={"source": config_entries.SOURCE_HOMEKIT}, data=zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + addresses=[IP_ADDRESS], hostname="mock_hostname", name="mock_name", port=None, @@ -536,6 +537,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): config_entries.SOURCE_HOMEKIT, zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + addresses=[IP_ADDRESS], hostname="mock_hostname", name="mock_name", port=None, @@ -603,6 +605,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): config_entries.SOURCE_HOMEKIT, zeroconf.ZeroconfServiceInfo( host=IP_ADDRESS, + addresses=[IP_ADDRESS], hostname="mock_hostname", name="mock_name", port=None, @@ -734,3 +737,47 @@ async def test_discovered_zeroconf(hass): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_discovery_updates_ip(hass: HomeAssistant): + """Test discovery updtes ip.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.2.3"}, unique_id=ID + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): + """Test discovery adds missing ip.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_ID: ID}) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 059a47b53a1c4..2e37daaf9dda3 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -219,8 +219,8 @@ async def _async_test_service( power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() - mocked_bulb.start_music.assert_called_once() - mocked_bulb.start_music.reset_mock() + mocked_bulb.async_start_music.assert_called_once() + mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) @@ -261,8 +261,8 @@ async def _async_test_service( power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() - mocked_bulb.start_music.assert_called_once() - mocked_bulb.start_music.reset_mock() + mocked_bulb.async_start_music.assert_called_once() + mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) @@ -304,7 +304,7 @@ async def _async_test_service( power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() - mocked_bulb.start_music.assert_called_once() + mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) @@ -322,7 +322,7 @@ async def _async_test_service( brightness = 100 color_temp = 200 transition = 1 - mocked_bulb.start_music.reset_mock() + mocked_bulb.async_start_music.reset_mock() mocked_bulb.async_set_brightness.reset_mock() mocked_bulb.async_set_color_temp.reset_mock() mocked_bulb.async_start_flow.reset_mock() @@ -348,7 +348,7 @@ async def _async_test_service( power_mode=PowerMode.NORMAL, ) mocked_bulb.async_turn_on.reset_mock() - mocked_bulb.start_music.assert_called_once() + mocked_bulb.async_start_music.assert_called_once() mocked_bulb.async_set_brightness.assert_called_once_with( brightness / 255 * 100, duration=transition * 1000, light_type=LightType.Main ) @@ -452,7 +452,7 @@ async def _async_test_service( ) # set_music_mode failure enable - mocked_bulb.start_music = MagicMock(side_effect=AssertionError) + mocked_bulb.async_start_music = MagicMock(side_effect=AssertionError) assert "Unable to turn on music mode, consider disabling it" not in caplog.text await hass.services.async_call( DOMAIN, @@ -460,14 +460,14 @@ async def _async_test_service( {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, blocking=True, ) - assert mocked_bulb.start_music.mock_calls == [call()] + assert mocked_bulb.async_start_music.mock_calls == [call()] assert "Unable to turn on music mode, consider disabling it" in caplog.text # set_music_mode disable await _async_test_service( SERVICE_SET_MUSIC_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "false"}, - "stop_music", + "async_stop_music", failure_side_effect=None, ) @@ -475,7 +475,7 @@ async def _async_test_service( await _async_test_service( SERVICE_SET_MUSIC_MODE, {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, - "start_music", + "async_start_music", failure_side_effect=None, ) # test _cmd wrapper error handler diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 24b6ec97ec64c..b7e99991fdd23 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -780,6 +780,15 @@ async def test_info_from_service_prefers_ipv4(hass): assert info.host == "192.168.66.12" +async def test_info_from_service_can_return_ipv6(hass): + """Test that IPv6-only devices can be discovered.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["fd11:1111:1111:0:1234:1234:1234:1234"] + info = zeroconf.info_from_service(service_info) + assert info.host == "fd11:1111:1111:0:1234:1234:1234:1234" + + async def test_get_instance(hass, mock_async_zeroconf): """Test we get an instance.""" assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -1095,6 +1104,7 @@ async def test_service_info_compatibility(hass, caplog): """ discovery_info = zeroconf.ZeroconfServiceInfo( host="mock_host", + addresses=["mock_host"], port=None, hostname="mock_hostname", type="mock_type", diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 84e66f0833a0d..e3742a3132f6a 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -6,6 +6,7 @@ import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -62,7 +63,7 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_away from HA cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -85,7 +86,7 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # trip alarm from faulty code entry cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_arm_away", {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -94,13 +95,13 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "1111"}, blocking=True, ) await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "1111"}, blocking=True, @@ -123,7 +124,7 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_home from HA cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_arm_home", {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -143,7 +144,7 @@ async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_devic # arm_night from HA cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_arm_night", {ATTR_ENTITY_ID: entity_id}, blocking=True, @@ -240,7 +241,7 @@ async def reset_alarm_panel(hass, cluster, entity_id): """Reset the state of the alarm panel.""" cluster.client_command.reset_mock() await hass.services.async_call( - Platform.ALARM_CONTROL_PANEL, + ALARM_DOMAIN, "alarm_disarm", {ATTR_ENTITY_ID: entity_id, "code": "4321"}, blocking=True, diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 4dc72b092e49e..8ff787af7bd42 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -25,6 +25,7 @@ CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, + DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, FAN_LOW, FAN_ON, @@ -525,7 +526,7 @@ async def test_target_temperature( entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -561,7 +562,7 @@ async def test_target_temperature_high( entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -597,7 +598,7 @@ async def test_target_temperature_low( entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) if preset: await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, blocking=True, @@ -628,7 +629,7 @@ async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): assert state.state == HVAC_MODE_OFF await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode}, blocking=True, @@ -647,7 +648,7 @@ async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): # turn off thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_OFF}, blocking=True, @@ -675,7 +676,7 @@ async def test_preset_setting(hass, device_climate_sinope): ] await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -692,7 +693,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] ] await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -709,7 +710,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x01\x01\x01")[0] ] await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, @@ -726,7 +727,7 @@ async def test_preset_setting(hass, device_climate_sinope): zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] ] await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, @@ -748,7 +749,7 @@ async def test_preset_setting_invalid(hass, device_climate_sinope): assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, blocking=True, @@ -769,7 +770,7 @@ async def test_set_temperature_hvac_mode(hass, device_climate): assert state.state == HVAC_MODE_OFF await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -809,7 +810,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): assert state.state == HVAC_MODE_HEAT_COOL await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -821,7 +822,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -843,7 +844,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): } await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -851,7 +852,7 @@ async def test_set_temperature_heat_cool(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -895,7 +896,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): assert state.state == HVAC_MODE_HEAT await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -912,7 +913,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -928,7 +929,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): } await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -936,7 +937,7 @@ async def test_set_temperature_heat(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, blocking=True, @@ -974,7 +975,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): assert state.state == HVAC_MODE_COOL await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: entity_id, @@ -991,7 +992,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): assert thrm_cluster.write_attributes.await_count == 0 await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, blocking=True, @@ -1007,7 +1008,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): } await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1015,7 +1016,7 @@ async def test_set_temperature_cool(hass, device_climate_mock): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, blocking=True, @@ -1057,7 +1058,7 @@ async def test_set_temperature_wrong_mode(hass, device_climate_mock): assert state.state == HVAC_MODE_DRY await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 24}, blocking=True, @@ -1080,7 +1081,7 @@ async def test_occupancy_reset(hass, device_climate_sinope): assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1133,7 +1134,7 @@ async def test_set_fan_mode_not_supported(hass, device_climate_fan): fan_cluster = device_climate_fan.device.endpoints[1].fan await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, blocking=True, @@ -1151,7 +1152,7 @@ async def test_set_fan_mode(hass, device_climate_fan): assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON}, blocking=True, @@ -1161,7 +1162,7 @@ async def test_set_fan_mode(hass, device_climate_fan): fan_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_AUTO}, blocking=True, @@ -1180,7 +1181,7 @@ async def test_set_moes_preset(hass, device_climate_moes): assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, blocking=True, @@ -1193,7 +1194,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, blocking=True, @@ -1209,7 +1210,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMFORT}, blocking=True, @@ -1225,7 +1226,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, blocking=True, @@ -1241,7 +1242,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_BOOST}, blocking=True, @@ -1257,7 +1258,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMPLEX}, blocking=True, @@ -1273,7 +1274,7 @@ async def test_set_moes_preset(hass, device_climate_moes): thrm_cluster.write_attributes.reset_mock() await hass.services.async_call( - Platform.CLIMATE, + CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, blocking=True, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index fe03f75930434..0c51ecffe9bf4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -51,6 +51,7 @@ async def test_discovery(detect_mock, hass): """Test zeroconf flow -- radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", + addresses=["192.168.1.200"], hostname="_tube_zb_gw._tcp.local.", name="mock_name", port=6053, @@ -95,6 +96,7 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.22", + addresses=["192.168.1.22"], hostname="tube_zb_gw_cc2652p2_poe.local.", name="mock_name", port=6053, @@ -127,6 +129,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.22", + addresses=["192.168.1.22"], hostname="tube_zb_gw_cc2652p2_poe.local.", name="mock_name", port=6053, @@ -389,6 +392,7 @@ async def test_discovery_already_setup(detect_mock, hass): """Test zeroconf flow -- radio detected.""" service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", + addresses=["192.168.1.200"], hostname="_tube_zb_gw._tcp.local.", name="mock_name", port=6053, diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 45c5928797dcf..73ab38c27acee 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -11,6 +11,7 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + DOMAIN as COVER_DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, @@ -140,7 +141,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x1, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -153,7 +154,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x0, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -166,7 +167,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x5, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -183,7 +184,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x2, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -226,7 +227,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # close from UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -237,7 +238,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x1, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -249,7 +250,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): await send_attributes_report(hass, cluster_level, {0: 0}) with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -261,7 +262,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x0, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -271,7 +272,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # set position UI command fails with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - Platform.COVER, + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -287,7 +288,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x5, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - Platform.COVER, + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, @@ -313,7 +314,7 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # test cover stop with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - Platform.COVER, + COVER_DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True, @@ -377,7 +378,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): with p1, p2: await hass.services.async_call( - Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False @@ -391,7 +392,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): with p1, p2: await hass.services.async_call( - Platform.COVER, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) await asyncio.sleep(0) assert cluster_on_off.request.call_count == 1 diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index e94c028acd899..a8f5f6450595f 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -14,6 +14,7 @@ ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, ATTR_SPEED, + DOMAIN as FAN_DOMAIN, SERVICE_SET_PRESET_MODE, SERVICE_SET_SPEED, SPEED_HIGH, @@ -246,7 +247,7 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): } await hass.services.async_call( - Platform.FAN, SERVICE_SET_PRESET_MODE, data, blocking=True + FAN_DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index ee437fc63c938..9c35215c889f9 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -9,7 +9,11 @@ import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f -from homeassistant.components.light import FLASH_LONG, FLASH_SHORT +from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, + FLASH_LONG, + FLASH_SHORT, +) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -327,7 +331,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): # turn on via UI cluster.request.reset_mock() await hass.services.async_call( - Platform.LIGHT, "turn_on", {"entity_id": entity_id}, blocking=True + LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 @@ -344,7 +348,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): # turn off via UI cluster.request.reset_mock() await hass.services.async_call( - Platform.LIGHT, "turn_off", {"entity_id": entity_id}, blocking=True + LIGHT_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 @@ -362,7 +366,7 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() # turn on via UI await hass.services.async_call( - Platform.LIGHT, "turn_on", {"entity_id": entity_id}, blocking=True + LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert on_off_cluster.request.call_count == 1 assert on_off_cluster.request.await_count == 1 @@ -375,7 +379,7 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() await hass.services.async_call( - Platform.LIGHT, + LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "transition": 10}, blocking=True, @@ -402,7 +406,7 @@ async def async_test_level_on_off_from_hass( level_cluster.request.reset_mock() await hass.services.async_call( - Platform.LIGHT, + LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "brightness": 10}, blocking=True, @@ -448,7 +452,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): # turn on via UI cluster.request.reset_mock() await hass.services.async_call( - Platform.LIGHT, + LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id, "flash": flash}, blocking=True, diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index c8685996c25cc..0669cebf128a7 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -7,6 +7,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( STATE_LOCKED, STATE_UNAVAILABLE, @@ -96,7 +97,7 @@ async def async_lock(hass, cluster, entity_id): ): # lock via UI await hass.services.async_call( - Platform.LOCK, "lock", {"entity_id": entity_id}, blocking=True + LOCK_DOMAIN, "lock", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False @@ -110,7 +111,7 @@ async def async_unlock(hass, cluster, entity_id): ): # lock via UI await hass.services.async_call( - Platform.LOCK, "unlock", {"entity_id": entity_id}, blocking=True + LOCK_DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index ac72a00d80235..336800f9ccbfa 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -7,6 +7,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.setup import async_setup_component @@ -109,7 +110,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi ): # set value via UI await hass.services.async_call( - Platform.NUMBER, + NUMBER_DOMAIN, "set_value", {"entity_id": entity_id, "value": 30.0}, blocking=True, diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 17e12491f8402..285bc1cd58596 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -13,6 +13,7 @@ ATTR_DURATION, ATTR_TONE, ATTR_VOLUME_LEVEL, + DOMAIN as SIREN_DOMAIN, ) from homeassistant.components.zha.core.const import ( WARNING_DEVICE_MODE_EMERGENCY_PANIC, @@ -72,7 +73,7 @@ async def test_siren(hass, siren): ): # turn on via UI await hass.services.async_call( - Platform.SIREN, "turn_on", {"entity_id": entity_id}, blocking=True + SIREN_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False @@ -92,7 +93,7 @@ async def test_siren(hass, siren): ): # turn off via UI await hass.services.async_call( - Platform.SIREN, "turn_off", {"entity_id": entity_id}, blocking=True + SIREN_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args[0][0] is False @@ -112,7 +113,7 @@ async def test_siren(hass, siren): ): # turn on via UI await hass.services.async_call( - Platform.SIREN, + SIREN_DOMAIN, "turn_on", { "entity_id": entity_id, diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 879bc26db9f22..c5cdf1a96f1e8 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -6,6 +6,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -136,7 +137,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ): # turn on via UI await hass.services.async_call( - Platform.SWITCH, "turn_on", {"entity_id": entity_id}, blocking=True + SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( @@ -150,7 +151,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ): # turn off via UI await hass.services.async_call( - Platform.SWITCH, "turn_off", {"entity_id": entity_id}, blocking=True + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( @@ -219,7 +220,7 @@ async def test_zha_group_switch_entity( ): # turn on via UI await hass.services.async_call( - Platform.SWITCH, "turn_on", {"entity_id": entity_id}, blocking=True + SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( @@ -234,7 +235,7 @@ async def test_zha_group_switch_entity( ): # turn off via UI await hass.services.async_call( - Platform.SWITCH, "turn_off", {"entity_id": entity_id}, blocking=True + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index 731e413caf819..265ec6f2d1ece 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -2,10 +2,15 @@ import datetime from unittest.mock import patch +import pytest + from homeassistant.components.zwave import binary_sensor, const from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_none(mock_openzwave): """Test device is not returned.""" diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 1afe961709745..a9ad182c4b16d 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -31,6 +31,9 @@ from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + @pytest.fixture def device(hass, mock_openzwave): diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index e8b784feefe73..e7283de25b469 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,6 +1,8 @@ """Test Z-Wave cover devices.""" from unittest.mock import MagicMock +import pytest + from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( CONF_INVERT_OPENCLOSE_BUTTONS, @@ -11,6 +13,9 @@ from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_none(hass, mock_openzwave): """Test device returns none.""" diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py index 18188cefcd630..d71ba0713d2d8 100644 --- a/tests/components/zwave/test_fan.py +++ b/tests/components/zwave/test_fan.py @@ -1,4 +1,6 @@ """Test Z-Wave fans.""" +import pytest + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -10,6 +12,9 @@ from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_fan(mock_openzwave): """Test get_device returns a zwave fan.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index b0114d087ad31..745d6d8ce5734 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -22,6 +22,9 @@ from tests.common import async_fire_time_changed, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + @pytest.fixture(autouse=True) def mock_storage(hass_storage): diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 74c541f4d5a0f..87bfb1ec72612 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,6 +1,8 @@ """Test Z-Wave lights.""" from unittest.mock import MagicMock, patch +import pytest + from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +20,9 @@ from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + class MockLightValues(MockEntityValues): """Mock Z-Wave light values.""" diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 04d46620013a2..575df9491adcf 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,11 +1,16 @@ """Test Z-Wave locks.""" from unittest.mock import MagicMock, patch +import pytest + from homeassistant import config_entries from homeassistant.components.zwave import const, lock from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_lock(mock_openzwave): """Test get_device returns a Z-Wave lock.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index c47201fb1682a..56ae0d61d41fc 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,11 +1,16 @@ """Test Z-Wave node entity.""" from unittest.mock import MagicMock, patch +import pytest + from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID import tests.mock.zwave as mock_zwave +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + async def test_maybe_schedule_update(hass, mock_openzwave): """Test maybe schedule update.""" diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index 83ebcaa3a4a4d..21944fe8f7ef2 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -1,10 +1,15 @@ """Test Z-Wave sensor.""" +import pytest + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.zwave import const, sensor import homeassistant.const from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_none(mock_openzwave): """Test get_device returns None.""" diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index 4293a4a23fd04..4c3efbe61fd51 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -1,10 +1,15 @@ """Test Z-Wave switches.""" from unittest.mock import patch +import pytest + from homeassistant.components.zwave import switch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_detects_switch(mock_openzwave): """Test get_device returns a Z-Wave switch.""" diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index 2ad94d29b0e79..2ffe5d617153d 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,6 +1,8 @@ """Test Z-Wave Websocket API.""" from unittest.mock import call, patch +import pytest + from homeassistant import config_entries from homeassistant.bootstrap import async_setup_component from homeassistant.components.zwave.const import ( @@ -14,6 +16,10 @@ NETWORK_KEY = "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + + async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): """Test Z-Wave websocket API.""" diff --git a/tests/components/zwave/test_workaround.py b/tests/components/zwave/test_workaround.py index ec708d38e43db..8f84fd6b94919 100644 --- a/tests/components/zwave/test_workaround.py +++ b/tests/components/zwave/test_workaround.py @@ -1,8 +1,13 @@ """Test Z-Wave workarounds.""" +import pytest + from homeassistant.components.zwave import const, workaround from tests.mock.zwave import MockNode, MockValue +# Integration is disabled +pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) + def test_get_device_no_component_mapping(): """Test that None is returned.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index d73daacdc7585..aea895d03bb13 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -37,5 +37,7 @@ ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights" METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh" METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v" +HUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_humidifier" +DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier" PROPERTY_ULTRAVIOLET = "Ultraviolet" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 4f21f616ae131..9696c922fb3c6 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -181,6 +181,12 @@ def controller_state_fixture(): return json.loads(load_fixture("zwave_js/controller_state.json")) +@pytest.fixture(name="controller_node_state", scope="session") +def controller_node_state_fixture(): + """Load the controller node state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_node_state.json")) + + @pytest.fixture(name="version_state", scope="session") def version_state_fixture(): """Load the version state fixture data.""" @@ -276,6 +282,12 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture(): ) +@pytest.fixture(name="climate_adc_t3000_state", scope="session") +def climate_adc_t3000_state_fixture(): + """Load the climate ADC-T3000 node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_adc_t3000_state.json")) + + @pytest.fixture(name="climate_danfoss_lc_13_state", scope="session") def climate_danfoss_lc_13_state_fixture(): """Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data.""" @@ -491,6 +503,12 @@ def zp3111_state_fixture(): return json.loads(load_fixture("zwave_js/zp3111-5_state.json")) +@pytest.fixture(name="express_controls_ezmultipli_state", scope="session") +def light_express_controls_ezmultipli_state_fixture(): + """Load the Express Controls EZMultiPli node state fixture data.""" + return json.loads(load_fixture("zwave_js/express_controls_ezmultipli_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -523,6 +541,14 @@ async def disconnect(): 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.""" @@ -610,6 +636,46 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( return node +@pytest.fixture(name="climate_adc_t3000") +def climate_adc_t3000_fixture(client, climate_adc_t3000_state): + """Mock a climate ADC-T3000 node.""" + node = Node(client, copy.deepcopy(climate_adc_t3000_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_adc_t3000_missing_setpoint") +def climate_adc_t3000_missing_setpoint_fixture(client, climate_adc_t3000_state): + """Mock a climate ADC-T3000 node with missing de-humidify setpoint.""" + data = copy.deepcopy(climate_adc_t3000_state) + data["name"] = f"{data['name']} missing setpoint" + for value in data["values"][:]: + if ( + value["commandClassName"] == "Humidity Control Setpoint" + and value["propertyKeyName"] == "De-humidifier" + ): + data["values"].remove(value) + node = Node(client, data) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_adc_t3000_missing_mode") +def climate_adc_t3000_missing_mode_fixture(client, climate_adc_t3000_state): + """Mock a climate ADC-T3000 node with missing mode setpoint.""" + data = copy.deepcopy(climate_adc_t3000_state) + data["name"] = f"{data['name']} missing mode" + for value in data["values"]: + if value["commandClassName"] == "Humidity Control Mode": + states = value["metadata"]["states"] + for key in list(states.keys()): + if states[key] == "De-humidify": + del states[key] + node = Node(client, data) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_danfoss_lc_13") def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): """Mock a climate radio danfoss LC-13 node.""" @@ -737,18 +803,6 @@ def null_name_check_fixture(client, null_name_check_state): return node -@pytest.fixture(name="multiple_devices") -def multiple_devices_fixture( - client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state -): - """Mock a client with multiple devices.""" - node = Node(client, copy.deepcopy(climate_radio_thermostat_ct100_plus_state)) - client.driver.controller.nodes[node.node_id] = node - node = Node(client, copy.deepcopy(lock_schlage_be469_state)) - client.driver.controller.nodes[node.node_id] = node - return client.driver.controller.nodes - - @pytest.fixture(name="gdc_zw062") def motorized_barrier_cover_fixture(client, gdc_zw062_state): """Mock a motorized barrier node.""" @@ -947,3 +1001,11 @@ def zp3111_fixture(client, zp3111_state): node = Node(client, copy.deepcopy(zp3111_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="express_controls_ezmultipli") +def express_controls_ezmultipli_fixture(client, express_controls_ezmultipli_state): + """Mock a Express Controls EZMultiPli node.""" + node = Node(client, copy.deepcopy(express_controls_ezmultipli_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json index c8d8f878c0b00..bf547556ac84e 100644 --- a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json +++ b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json @@ -10,7 +10,7 @@ "generic": {"key": 16, "label":"Binary Switch"}, "specific": {"key": 1, "label":"Binary Power Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -50,10 +50,10 @@ "nodeId": 102, "index": 0, "installerIcon": 1792, - "userIcon": 1792 + "userIcon": 1792, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, @@ -1245,5 +1245,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 27c3f991d3333..cbd11c668701f 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -10,7 +10,7 @@ "generic": {"key": 8, "label":"Thermostat"}, "specific": {"key": 6, "label":"Thermostat General V2"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -616,5 +616,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 5616abd6e0f7f..59e4fdfc9fb6e 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -3752,5 +3752,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 58608131e905f..b0dba3c6d0523 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -10,7 +10,7 @@ "generic": {"key": 17, "label":"Multilevel Switch"}, "specific": {"key": 1, "label":"Multilevel Power Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -645,5 +645,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index cf7adddc21e1c..2b8477b597fbe 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -10,7 +10,7 @@ "generic": {"key": 17, "label":"Multilevel Switch"}, "specific": {"key": 7, "label":"Motor Control Class C"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -397,5 +397,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json new file mode 100644 index 0000000000000..ba55aadd98c8d --- /dev/null +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -0,0 +1,4120 @@ +{ + "nodeId": 68, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 400, + "productId": 1, + "productType": 6, + "firmwareVersion": "1.44", + "zwavePlusVersion": 1, + "name": "ADC-T3000", + "deviceConfig": { + "filename": "/data/store/config/adc-t3000.json", + "isEmbedded": false, + "manufacturer": "Building 36 Technologies", + "manufacturerId": 400, + "label": "ADC-T 3000", + "description": "Alarm.com Smart Thermostat", + "devices": [ + { + "productType": 6, + "productId": 1 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ADC-T 3000", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 68, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [ + 32, + 114, + 64, + 67, + 134 + ], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 11, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "\u00b0F" + }, + "value": 72, + "nodeId": 68, + "newValue": 73, + "prevValue": 72.5 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 11, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%" + }, + "value": 34, + "nodeId": 68, + "newValue": 34, + "prevValue": 34 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Voltage", + "propertyName": "Voltage", + "ccVersion": 11, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Voltage", + "ccSpecific": { + "sensorType": 15, + "scale": 0 + }, + "unit": "V" + }, + "value": 3.034 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "3": "Auto" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "min": 35, + "max": 95, + "unit": "\u00b0F" + }, + "value": 60.8 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "min": 50, + "max": 95, + "unit": "\u00b0F" + }, + "value": 80 + }, + { + "endpoint": 0, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat fan mode", + "min": 0, + "max": 255, + "states": { + "0": "Auto low", + "1": "Low", + "6": "Circulation" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 68, + "commandClassName": "Thermostat Fan Mode", + "property": "off", + "propertyName": "off", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Thermostat fan turned off" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 69, + "commandClassName": "Thermostat Fan State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Thermostat fan state", + "min": 0, + "max": 255, + "states": { + "0": "Idle / off", + "1": "Running / running low", + "2": "Running high", + "3": "Running medium", + "4": "Circulation mode", + "5": "Humidity circulation mode", + "6": "Right - left circulation mode", + "7": "Up - down circulation mode", + "8": "Quiet circulation mode" + } + }, + "value": 0, + "newValue": 1, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 100, + "commandClassName": "Humidity Control Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Humidifier", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "min": 10, + "max": 70, + "unit": "%" + }, + "value": 35 + }, + { + "endpoint": 0, + "commandClass": 100, + "commandClassName": "Humidity Control Setpoint", + "property": "setpointScale", + "propertyKey": 1, + "propertyName": "setpointScale", + "propertyKeyName": "1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "states": { + "0": "%" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 100, + "commandClassName": "Humidity Control Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "De-humidifier", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "min": 30, + "max": 90, + "unit": "%" + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 100, + "commandClassName": "Humidity Control Setpoint", + "property": "setpointScale", + "propertyKey": 2, + "propertyName": "setpointScale", + "propertyKeyName": "2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "states": { + "0": "%" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 109, + "commandClassName": "Humidity Control Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity control mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Humidify", + "2": "De-humidify", + "3": "Auto" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 110, + "commandClassName": "Humidity Control Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity control operating state", + "min": 0, + "max": 255, + "states": { + "0": "Idle", + "1": "Humidifying", + "2": "De-humidifying" + } + }, + "value": 0, + "newValue": 1, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "HVAC System Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Configures the type of heating system used.", + "label": "HVAC System Type", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Normal", + "1": "Heat Pump" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Number of Heat Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Heat Stages 0-3 Default is 2.", + "label": "Number of Heat Stages", + "default": 2, + "min": 0, + "max": 3, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Number of Cool Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Cool Stages 0-2 Default is 2.", + "label": "Number of Cool Stages", + "default": 2, + "min": 0, + "max": 2, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Heat Fuel Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Choose type of fuel. Reality - whether unit is boiler vs forced air.", + "label": "Heat Fuel Type", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Fossil Fuel", + "1": "Electric" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyKey": 16776960, + "propertyName": "Calibration Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: -10 to 10 in 1 \u00b0F increments.", + "label": "Calibration Temperature", + "default": 0, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyKey": 16776960, + "propertyName": "Swing", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 3 in 0.5 \u00b0F increments.", + "label": "Swing", + "default": 50, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyKey": 16776960, + "propertyName": "Overshoot", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 3 in 0.5 \u00b0F increments.", + "label": "Overshoot", + "default": 0, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Heat Staging Delay", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heat Staging Delay", + "default": 30, + "min": 1, + "max": 60, + "unit": "minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Cool Staging Delay", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Cool Staging Delay", + "default": 30, + "min": 1, + "max": 60, + "unit": "minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyKey": 16776960, + "propertyName": "Balance Setpoint", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 95 in 1 \u00b0F increments.", + "label": "Balance Setpoint", + "default": 300, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 300 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Fan Circulation Period", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Fan Circulation Period", + "default": 60, + "min": 10, + "max": 1440, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Fan Circulation Duty Cycle", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Fan Circulation Duty Cycle", + "default": 25, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 25 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Fan Purge Time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Fan Purge Time", + "default": 60, + "min": 1, + "max": 3600, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyKey": 16776960, + "propertyName": "Maximum Heat Setpoint", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 35 to 95 in 1 \u00b0F increments.", + "label": "Maximum Heat Setpoint", + "default": 950, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 950 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyKey": 16776960, + "propertyName": "Minimum Heat Setpoint", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 35 to 95 in 1 \u00b0F increments.", + "label": "Minimum Heat Setpoint", + "default": 350, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 350 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyKey": 16776960, + "propertyName": "Maximum Cool Setpoint", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 50 to 95 in 1 \u00b0F increments.", + "label": "Maximum Cool Setpoint", + "default": 950, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 950 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyKey": 16776960, + "propertyName": "Minimum Cool Setpoint", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 50 to 95 in 1 \u00b0F increments.", + "label": "Minimum Cool Setpoint", + "default": 500, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 500 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Thermostat Lock", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Lock out physical thermostat controls.", + "label": "Thermostat Lock", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "Disabled", + "1": "Full Lock", + "2": "Partial Lock" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Compressor Delay", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Compressor Delay", + "default": 5, + "min": 0, + "max": 60, + "unit": "minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyName": "Temperature Display Units", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Celsius or Farenheit for temperature display.", + "label": "Temperature Display Units", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Celsius", + "1": "Farenheit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 24, + "propertyName": "HVAC Modes Enabled", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Which heating/cooling modes are available.", + "label": "HVAC Modes Enabled", + "default": 15, + "min": 3, + "max": 31, + "states": { + "3": "Off, Heat", + "5": "Off, Cool", + "7": "Off, Heat, Cool", + "15": "Off, Heat, Cool, Auto", + "19": "Off, Heat, Emergency Heat", + "23": "Off, Heat, Cool, Emergency Heat", + "31": "Off, Heat, Cool, Auto, Emergency Heat" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 15 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 25, + "propertyKey": 255, + "propertyName": "Configurable Terminal Setting Z2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Changes control of configurable terminal", + "label": "Configurable Terminal Setting Z2", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "None", + "1": "W3, 3rd Stage Auxiliary Heat", + "2": "H, Humidifier", + "3": "DH, Dehumidifier", + "4": "External Air Baffle or Vent" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 25, + "propertyKey": 65280, + "propertyName": "Configurable Terminal Setting Z1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Changes control of configurable terminal", + "label": "Configurable Terminal Setting Z1", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "None", + "1": "W3, 3rd Stage Auxiliary Heat", + "2": "H, Humidifier", + "3": "DH, Dehumidifier", + "4": "External Air Baffle or Vent" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 26, + "propertyName": "Power Source", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Which source of power is utilized.", + "label": "Power Source", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Battery", + "1": "C-Wire" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 27, + "propertyName": "Battery Alert Threshold Low", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Battery Alert Range", + "label": "Battery Alert Threshold Low", + "default": 30, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 28, + "propertyName": "Battery Alert Threshold Very Low", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Very Low Battery Alert Range (percentage)", + "label": "Battery Alert Threshold Very Low", + "default": 15, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 15 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 2147483648, + "propertyName": "Current Relay State: Z1 Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Z1 Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 1073741824, + "propertyName": "Current Relay State: Y2 Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Y2 Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 536870912, + "propertyName": "Current Relay State: Y Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Y Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 268435456, + "propertyName": "Current Relay State: W2 Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: W2 Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 134217728, + "propertyName": "Current Relay State: W Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: W Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 67108864, + "propertyName": "Current Relay State: G Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: G Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 33554432, + "propertyName": "Current Relay State: O Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: O Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 8388608, + "propertyName": "Current Relay State: Override Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Override Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 1048576, + "propertyName": "Current Relay State: C Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: C Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 524288, + "propertyName": "Current Relay State: RC Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: RC Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 262144, + "propertyName": "Current Relay State: RH Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: RH Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 131072, + "propertyName": "Current Relay State: Z2 Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Z2 Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 65536, + "propertyName": "Current Relay State: B Terminal Load", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: B Terminal Load", + "min": 0, + "max": 1, + "states": { + "0": "No Load", + "1": "Load" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 32768, + "propertyName": "Current Relay State: Z1 Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Z1 Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 16384, + "propertyName": "Current Relay State: Y2 Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Y2 Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 8192, + "propertyName": "Current Relay State: Y Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Y Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 4096, + "propertyName": "Current Relay State: W2 Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: W2 Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 2048, + "propertyName": "Current Relay State: W Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: W Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 1024, + "propertyName": "Current Relay State: G Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: G Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 512, + "propertyName": "Current Relay State: O Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: O Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 8, + "propertyName": "Current Relay State: RC Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: RC Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 4, + "propertyName": "Current Relay State: RH Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: RH Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1, + "newValue": 1, + "prevValue": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 2, + "propertyName": "Current Relay State: Z2 Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: Z2 Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 29, + "propertyKey": 1, + "propertyName": "Current Relay State: B Relay State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current Relay State: B Relay State", + "min": 0, + "max": 1, + "states": { + "0": "Not closed", + "1": "Closed" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0, + "newValue": 0, + "prevValue": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 255, + "propertyName": "Remote Temperature Enable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enables remote temperature sensor instead of built-in.", + "label": "Remote Temperature Enable", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 30, + "propertyKey": 65280, + "propertyName": "Remote Temperature Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Status of the remote temperature sensor.", + "label": "Remote Temperature Status", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "Remote temperature disabled", + "1": "Active and functioning properly", + "2": "Inactive, timeout reached (see parameter 39)", + "3": "Inactive, temperature differential reached (see parameter 40)", + "4": "Inactive, 3 successive communication attempts failed" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 31, + "propertyKey": 16776960, + "propertyName": "Heat Differential", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 1 to 10 in 0.5 \u00b0F increments.", + "label": "Heat Differential", + "default": 30, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 32, + "propertyKey": 16776960, + "propertyName": "Cool Differential", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 1 to 10 in 0.5 \u00b0F increments.", + "label": "Cool Differential", + "default": 30, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 33, + "propertyKey": 16776960, + "propertyName": "Temperature Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0.5 to 2 in 0.5 \u00b0F increments.", + "label": "Temperature Reporting Threshold", + "default": 10, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 35, + "propertyName": "Z-Wave Echo Association Reports", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Enable/Disabled Echo Assoc. Reports.", + "label": "Z-Wave Echo Association Reports", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 36, + "propertyKey": 16776960, + "propertyName": "C-Wire Power Thermistor Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: -10 to 10 in 0.1 \u00b0F increments.", + "label": "C-Wire Power Thermistor Offset", + "default": -20, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": -10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 37, + "propertyName": "Run Fan With Auxiliary Heat", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Run Fan With Auxiliary Heat", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 1, + "propertyName": "Z-Wave Association Report: Thermostat Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Thermostat Mode", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 8, + "propertyName": "Z-Wave Association Report: Thermostat Operating State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Thermostat Operating State", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 16, + "propertyName": "Z-Wave Association Report: Thermostat Fan Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Thermostat Fan Mode", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 32, + "propertyName": "Z-Wave Association Report: Thermostat Fan State", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Thermostat Fan State", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 64, + "propertyName": "Z-Wave Association Report: Ambiant Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Ambiant Temperature", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 128, + "propertyName": "Z-Wave Association Report: Relative Humidity", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Relative Humidity", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 512, + "propertyName": "Z-Wave Association Report: Battery Low Notification", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Battery Low Notification", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 1024, + "propertyName": "Z-Wave Association Report: Battery Very Low Notification", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Battery Very Low Notification", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 2048, + "propertyName": "Z-Wave Association Report: Thermostat Supported Modes", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Thermostat Supported Modes", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 4096, + "propertyName": "Z-Wave Association Report: Remote Enable Report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Remote Enable Report", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 8192, + "propertyName": "Z-Wave Association Report: Humidity Control Operating State Report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Humidity Control Operating State Report", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 16384, + "propertyName": "Z-Wave Association Report: HVAC Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: HVAC Type", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 32768, + "propertyName": "Z-Wave Association Report: Number of Cool/Pump Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Number of Cool/Pump Stages", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 65536, + "propertyName": "Z-Wave Association Report: Number of Heat/Aux Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Number of Heat/Aux Stages", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 131072, + "propertyName": "Z-Wave Association Report: Relay Status", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Relay Status", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 262144, + "propertyName": "Z-Wave Association Report: Power Source", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Power Source", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 524288, + "propertyName": "Z-Wave Association Report: Notification Report Power Applied", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report Power Applied", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 1048576, + "propertyName": "Z-Wave Association Report: Notification Report Mains Disconnected", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report Mains Disconnected", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 2097152, + "propertyName": "Z-Wave Association Report: Notification Report Mains Reconnected", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report Mains Reconnected", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 4194304, + "propertyName": "Z-Wave Association Report: Notification Report Replace Battery Soon", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report Replace Battery Soon", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 8388608, + "propertyName": "Z-Wave Association Report: Notification Report Replace Battery Now", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report Replace Battery Now", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 16777216, + "propertyName": "Z-Wave Association Report: Notification Report System Hardware Failure", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report System Hardware Failure", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 33554432, + "propertyName": "Z-Wave Association Report: Notification Report System Software Failure", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report System Software Failure", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 67108864, + "propertyName": "Z-Wave Association Report: Notification Report System Hardware Failure with Code", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report System Hardware Failure with Code", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 134217728, + "propertyName": "Z-Wave Association Report: Notification Report System Software Failure with Code", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Notification Report System Software Failure with Code", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 268435456, + "propertyName": "Z-Wave Association Report: Display Units", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Display Units", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 536870912, + "propertyName": "Z-Wave Association Report: Heat Fuel Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Heat Fuel Type", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 1073741824, + "propertyName": "Z-Wave Association Report: Humidity Control Mode", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Humidity Control Mode", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 38, + "propertyKey": 2147483648, + "propertyName": "Z-Wave Association Report: Humidity Control Setpoints", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Bitmask to selectively enable non-required Z-wave association reports.", + "label": "Z-Wave Association Report: Humidity Control Setpoints", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Remote Temperature Timeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Remote Temperature Timeout", + "default": 130, + "min": 0, + "max": 32767, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 130 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyKey": 16776960, + "propertyName": "Remote Temperature Differential", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 99 in 1 \u00b0F increments.", + "label": "Remote Temperature Differential", + "default": 250, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 250 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 41, + "propertyName": "Remote Temperature ACK Failure Limit", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Remote Temperature ACK Failure Limit", + "default": 3, + "min": 0, + "max": 127, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Remote Temperature Display Enable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Remote Temperature Display Enable", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 43, + "propertyName": "Outdoor Temperature Timeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Outdoor Temperature Timeout", + "default": 1440, + "min": 0, + "max": 32767, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1440 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 45, + "propertyName": "Heat Pump Expire", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heat Pump Expire", + "default": 0, + "min": 0, + "max": 2880, + "unit": "minutes", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 46, + "propertyKey": 16776960, + "propertyName": "Dehumidify by AC Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 10 in 1 \u00b0F increments.", + "label": "Dehumidify by AC Offset", + "default": 30, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 48, + "propertyName": "PIR Enable", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "PIR Enable", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 49, + "propertyName": "Humidity Display", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity Display", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 2147483648, + "propertyName": "System configuration: Aux Fan", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Aux Fan", + "min": 0, + "max": 1, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 1610612736, + "propertyName": "System configuration: Cool Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Cool Stages", + "min": 0, + "max": 3, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 402653184, + "propertyName": "System configuration: Heat Stages", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Heat Stages", + "min": 0, + "max": 3, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 67108864, + "propertyName": "System configuration: Fuel", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Fuel", + "min": 0, + "max": 1, + "states": { + "0": "Fuel", + "1": "Electric" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 50331648, + "propertyName": "System configuration: HVAC Type", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: HVAC Type", + "min": 0, + "max": 3, + "states": { + "0": "Normal", + "1": "Heat Pump" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 15728640, + "propertyName": "System configuration: Z2 Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Z2 Configuration", + "min": 0, + "max": 15, + "states": { + "0": "None", + "1": "W3, 3rd Stage Auxiliary Heat", + "2": "H, Humidifier", + "3": "DH, Dehumidifier", + "4": "External Air Baffle or Vent" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 983040, + "propertyName": "System configuration: Z1 Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Z1 Configuration", + "min": 0, + "max": 15, + "states": { + "0": "None", + "1": "W3, 3rd Stage Auxiliary Heat", + "2": "H, Humidifier", + "3": "DH, Dehumidifier", + "4": "External Air Baffle or Vent" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 50, + "propertyKey": 256, + "propertyName": "System configuration: Override", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "Summarized report of system configuration", + "label": "System configuration: Override", + "min": 0, + "max": 1, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 52, + "propertyName": "Vent Options", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Vent Options", + "default": 4, + "min": 0, + "max": 4, + "states": { + "0": "Disabled", + "1": "Always activate regardless of thermostat operating state", + "2": "Only activate when heating", + "3": "Only activate when cooling", + "4": "Only activate when heating or cooling" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 53, + "propertyName": "Vent Circulation Period", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Vent Circulation Period", + "default": 60, + "min": 10, + "max": 1440, + "unit": "minutes", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 54, + "propertyName": "Vent Circulation Duty Cycle", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Vent Circulation Duty Cycle", + "default": 25, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 25 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 55, + "propertyKey": 16776960, + "propertyName": "Vent Maximum Outdoor Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 99 in 1 \u00b0F increments.", + "label": "Vent Maximum Outdoor Temperature", + "default": -32768, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": -1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 56, + "propertyKey": 16776960, + "propertyName": "Vent Minimum Outdoor Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0 to 99 in 1 \u00b0F increments.", + "label": "Vent Minimum Outdoor Temperature", + "default": -32768, + "min": -32768, + "max": 32767, + "states": { + "-1": "Disabled" + }, + "unit": "0.1\u00b0F", + "valueSize": 4, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": -1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 57, + "propertyName": "Relay Harvest Level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Relay Harvest Level", + "default": 12, + "min": 0, + "max": 12, + "states": { + "0": "Off", + "9": "8 pulses", + "10": "16 pulses", + "11": "32 pulses", + "12": "64 pulses" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 58, + "propertyName": "Relay Harvest Interval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Relay Harvest Interval", + "default": 4, + "min": 0, + "max": 5, + "states": { + "0": "Off", + "2": "4 Milliseconds", + "3": "8 Milliseconds", + "4": "16 Milliseconds", + "5": "32 Milliseconds" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 59, + "propertyName": "Minimum Battery Reporting Interval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Minimum number of hours between battery reports", + "label": "Minimum Battery Reporting Interval", + "default": 60, + "min": 0, + "max": 127, + "unit": "hours", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 60, + "propertyName": "Humidity Control Swing", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Percent value the thermostat will add (for de-humidify) to or remove (for humidify) from the relevant humidity control setpoint.", + "label": "Humidity Control Swing", + "default": 5, + "min": 1, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 61, + "propertyName": "Humidity Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The minimum percent the relative humidity must change between reported humidity values.", + "label": "Humidity Reporting Threshold", + "default": 5, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 62, + "propertyName": "Z-Wave Send Fail Limit", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Z-Wave Send Fail Limit", + "default": 10, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 64, + "propertyName": "Vent Override Lockout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Activate the vent if it has not been active in the specified period.", + "label": "Vent Override Lockout", + "default": 12, + "min": 0, + "max": 127, + "unit": "hours", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 12 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 65, + "propertyName": "Humidify Options", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidify Options", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Always humidify regardless of thermostat operating state", + "1": "Only humidify when the thermostat operating state is heating, when in heat mode or when heating in auto mode. When in any other thermostat mode, the thermostat will humidify whenever it is necessary." + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 51, + "propertyName": "Thermostat Reset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "description": "Must write the magic value 2870 to take effect.", + "label": "Thermostat Reset", + "default": 0, + "min": 0, + "max": 2870, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Power status", + "propertyName": "Power Management", + "propertyKeyName": "Power status", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Power has been applied" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "10": "Replace battery soon", + "11": "Replace battery now" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Hardware status", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Hardware status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "System hardware failure", + "3": "System hardware failure (with failure code)" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Software status", + "propertyName": "System", + "propertyKeyName": "Software status", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Software status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "2": "System software failure", + "4": "System software failure (with failure code)" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 7, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.4" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.44", + "1.40", + "1.30" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 8 + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [ + 40000, + 100000 + ], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [ + 32, + 114, + 64, + 67, + 134 + ], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": true + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": true + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": true + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 3, + "isSecure": true + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 100, + "name": "Humidity Control Setpoint", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 109, + "name": "Humidity Control Mode", + "version": 2, + "isSecure": true + }, + { + "id": 110, + "name": "Humidity Control Operating State", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 7, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0190:0x0006:0x0001:1.44", + "statistics": { + "commandsTX": 6, + "commandsRX": 6124, + "commandsDroppedRX": 40, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false +} \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index 8574674714f07..206a32df6640f 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -432,5 +432,6 @@ "1.1" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index 8dff31a52256a..1241e0b35d745 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -25,7 +25,7 @@ "Thermostat Setpoint", "Version" ], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -712,5 +712,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index 2526e346a5362..8c655d503ed79 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -1440,5 +1440,6 @@ "isSecure": false } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 50886b504a713..75d8bb99e5551 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -1246,5 +1246,6 @@ "isSecure": true } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index b26b69be9ad5e..0ac4c6ab696ed 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -10,7 +10,7 @@ "generic": {"key": 8, "label":"Thermostat"}, "specific": {"key": 6, "label":"Thermostat General V2"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -1173,5 +1173,6 @@ }, "value": 25.5 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index 52f2168fd83c4..8bfe3a3f7af9d 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -826,5 +826,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index f940dd210aa08..398371a744542 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1083,5 +1083,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index cd5a6bd4abe6e..b81acf66b803d 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -6,11 +6,11 @@ "status": 4, "ready": true, "deviceClass": { - "basic": {"key": 2, "label":"Static Controller"}, - "generic": {"key": 8, "label":"Thermostat"}, - "specific": {"key": 6, "label":"Thermostat General V2"}, + "basic": { "key": 2, "label": "Static Controller" }, + "generic": { "key": 8, "label": "Thermostat" }, + "specific": { "key": 6, "label": "Thermostat General V2" }, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -47,7 +47,129 @@ "nodeId": 13, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 13, @@ -57,128 +179,6 @@ }, { "nodeId": 13, "index": 2 } ], - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 5, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "values": [ { "commandClassName": "Manufacturer Specific", @@ -851,5 +851,6 @@ }, "value": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index 5feaa247f2ef9..5c8a12a683268 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -958,5 +958,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/controller_node_state.json b/tests/components/zwave_js/fixtures/controller_node_state.json new file mode 100644 index 0000000000000..1f3c71971bc65 --- /dev/null +++ b/tests/components/zwave_js/fixtures/controller_node_state.json @@ -0,0 +1,104 @@ +{ + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z\u2010Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false +} diff --git a/tests/components/zwave_js/fixtures/controller_state.json b/tests/components/zwave_js/fixtures/controller_state.json index d4bf58a53ceb7..ac0cedcffef9b 100644 --- a/tests/components/zwave_js/fixtures/controller_state.json +++ b/tests/components/zwave_js/fixtures/controller_state.json @@ -92,7 +92,8 @@ ], "sucNodeId": 1, "supportsTimers": false, - "isHealNetworkActive": false + "isHealNetworkActive": false, + "inclusionState": 0 }, "nodes": [ ] diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index b5373f38ec447..7959378a7ad46 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -494,5 +494,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 59dff9458464e..6d4defbd42c43 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -1129,5 +1129,6 @@ "commandsDroppedRX": 1, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index 35ce70f617aa7..4d10577a2d1d2 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -10,7 +10,7 @@ "generic": {"key": 17, "label":"Routing Slave"}, "specific": {"key": 0, "label":"Unused"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -353,5 +353,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index bde7c90e1e4ce..913f24d41aeff 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -896,5 +896,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index 9e7b05adc34bf..47aafdfd0a424 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -10,7 +10,7 @@ "generic": {"key": 64, "label":"Entry Control"}, "specific": {"key": 7, "label":"Secure Barrier Add-on"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -917,5 +917,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index b11d2bfd18089..a1806a99ce0f7 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -10,7 +10,7 @@ "generic": {"key": 17, "label":"Routing Slave"}, "specific": {"key": 1, "label":"Multilevel Power Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -775,5 +775,6 @@ "value": 0, "ccVersion": 3 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index 9c2befdf5e8df..444b7eafc679c 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -8,7 +8,7 @@ "generic": {"key": 32, "label":"Binary Sensor"}, "specific": {"key": 1, "label":"Routing Binary Sensor"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": false, @@ -325,6 +325,7 @@ "2.0" ] } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json new file mode 100644 index 0000000000000..ea267d86b8ce3 --- /dev/null +++ b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json @@ -0,0 +1,673 @@ +{ + "nodeId": 96, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 30, + "productId": 1, + "productType": 4, + "firmwareVersion": "1.8", + "zwavePlusVersion": 1, + "name": "HSM200", + "location": "Basement", + "deviceConfig": { + "filename": "/data/db/devices/0x001e/ezmultipli.json", + "isEmbedded": true, + "manufacturer": "Express Controls", + "manufacturerId": 30, + "label": "EZMultiPli", + "description": "Multi Sensor", + "devices": [ + { + "productType": 4, + "productId": 1 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "EZMultiPli", + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 96, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "7": "Motion detection (location provided)" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 6, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 16.8 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Illuminance", + "propertyName": "Illuminance", + "ccVersion": 6, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 0 + }, + "unit": "%" + }, + "value": 61 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Remaining duration" + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Red color.", + "label": "Current value (Red)", + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Green color.", + "label": "Current value (Green)", + "min": 0, + "max": 255 + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Blue color.", + "label": "Current value (Blue)", + "min": 0, + "max": 255 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyName": "currentColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Current Color" + }, + "value": { + "red": 0, + "green": 255, + "blue": 0 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "hexColor", + "propertyName": "hexColor", + "ccVersion": 1, + "metadata": { + "type": "color", + "readable": true, + "writeable": true, + "label": "RGB Color", + "minLength": 6, + "maxLength": 7 + }, + "value": "00ff00" + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Red color.", + "label": "Target value (Red)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Green color.", + "label": "Target value (Green)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Blue color.", + "label": "Target value (Blue)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Target Color", + "valueChangeOptions": ["transitionDuration"] + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.8"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "OnTime", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "OnTime", + "default": 10, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "OnLevel", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 0-99, 255", + "label": "OnLevel", + "default": 255, + "min": 0, + "max": 255, + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "LiteMin", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LiteMin", + "default": 60, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "TempMin", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "A Temperature report is sent to the controller every TempMin minutes.", + "label": "TempMin", + "default": 60, + "min": 0, + "max": 127, + "unit": "Minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "TempAdj", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "TempAdj", + "default": 0, + "min": -128, + "max": 127, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": -40 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 6, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 2, + "isSecure": false + }, + { + "id": 119, + "name": "Node Naming and Location", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + } + ], + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001e:0x0004:0x0001:1.8", + "statistics": { + "commandsTX": 147, + "commandsRX": 322, + "commandsDroppedRX": 0, + "commandsDroppedTX": 3, + "timeoutResponse": 0 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index b6cf59b422605..fa4c96d439ab7 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -8,7 +8,7 @@ "generic": {"key": 17, "label":"Multilevel Switch"}, "specific": {"key": 1, "label":"Multilevel Power Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -427,5 +427,6 @@ "3.10" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index 1f4f55dd22017..fc89976d14a92 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -10,7 +10,7 @@ "generic": {"key": 17, "label":"Multilevel Switch"}, "specific": {"key": 8, "label":"Fan Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -348,5 +348,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index f83a1193c2295..edab052af5b3b 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -10502,5 +10502,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index d8973f2688e37..8c88082718c54 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -346,5 +346,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index fb31f83866712..aa0e05dd47fdd 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -351,5 +351,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 1 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json index 58d3f0d06eca0..4cbf9ef1ce43a 100644 --- a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json +++ b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json @@ -64,7 +64,87 @@ }, "mandatorySupportedCCs": [32, 38, 39], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 32, + "name": "Basic", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + } + ] } ], "values": [ @@ -557,86 +637,7 @@ "mandatorySupportedCCs": [32, 38, 39], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 32, - "name": "Basic", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 2, - "isSecure": false - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": false - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 86, - "name": "CRC-16 Encapsulation", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - } - ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json index e5f739d63a5f1..926285e535901 100644 --- a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json +++ b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json @@ -10,7 +10,7 @@ "generic": {"key": 16, "label":"Binary Switch"}, "specific": {"key": 1, "label":"Binary Power Switch"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -60,10 +60,10 @@ "nodeId": 32, "index": 0, "installerIcon": 1792, - "userIcon": 1792 + "userIcon": 1792, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Binary Switch", @@ -720,5 +720,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index bfa5689141337..11e88eff8be64 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -1952,5 +1952,6 @@ } } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 213b873f85ca8..b244913070c1d 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -685,5 +685,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index 2b218cd915b27..642682766df15 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -10,7 +10,7 @@ "generic": {"key": 64, "label":"Entry Control"}, "specific": {"key": 3, "label":"Secure Keypad Door Lock"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -446,5 +446,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index f5e66b7e7a667..5bd4cfc80806b 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -2915,5 +2915,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 2b4a3a8898401..dc6e9e40d7ca8 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -564,5 +564,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json index f85a8e6b0056a..64f83a43e0d8f 100644 --- a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json +++ b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json @@ -8,7 +8,7 @@ "generic": {"key": 64, "label":"Entry Control"}, "specific": {"key": 3, "label":"Secure Keypad Door Lock"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -47,71 +47,71 @@ "endpoints": [ { "nodeId": 20, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], - "commandClasses": [ - { - "id": 98, - "name": "Door Lock", - "version": 1, - "isSecure": true - }, - { - "id": 99, - "name": "User Code", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 1, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "values": [ { "commandClassName": "Door Lock", @@ -2103,5 +2103,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/multisensor_6_state.json b/tests/components/zwave_js/fixtures/multisensor_6_state.json index 88cdf893d4a55..634d8ec916990 100644 --- a/tests/components/zwave_js/fixtures/multisensor_6_state.json +++ b/tests/components/zwave_js/fixtures/multisensor_6_state.json @@ -10,7 +10,7 @@ "generic": {"key": 21, "label":"Multilevel Sensor"}, "specific": {"key": 1, "label":"Routing Multilevel Sensor"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": true, "isFrequentListening": false, @@ -1825,5 +1825,6 @@ } } ], - "highestSecurityClass": 7 + "highestSecurityClass": 7, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index 98ae03afbf2e4..39c04216a0441 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -11,7 +11,7 @@ "generic": {"key": 8, "label":"Thermostat"}, "specific": {"key": 6, "label":"Thermostat General V2"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "neighbors": [], "interviewAttempts": 1, @@ -250,7 +250,8 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false }, "result": {} } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json index 01bad6c4a8fb1..44b1379ca82e4 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json @@ -11,7 +11,7 @@ "generic": {"key": 8, "label":"Thermostat"}, "specific": {"key": 6, "label":"Thermostat General V2"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -274,5 +274,6 @@ } } ] - } + }, + "replaced": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index 4e6ca17e01386..a99303af259c5 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -10,7 +10,7 @@ "generic": {"key": 8, "label":"Thermostat"}, "specific": {"key": 6, "label":"Thermostat General V2"}, "mandatorySupportedCCs": [], - "mandatoryControlCCs": [] + "mandatoryControlledCCs": [] }, "isListening": false, "isFrequentListening": true, @@ -1275,5 +1275,6 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index fe63eaee20787..8905e47b155dd 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -410,5 +410,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index a2fdaa995614b..d1db5664f76f9 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -258,5 +258,6 @@ "2.0" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index d37e82ea3af83..f7abbffb590c2 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -429,5 +429,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json index 22eb05c9ce5d2..af5314002fa3a 100644 --- a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json +++ b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json @@ -80,98 +80,98 @@ "aggregatedEndpointCount": 0, "interviewAttempts": 1, "interviewStage": "NodeInfo", - "commandClasses": [ - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 2, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 35, "index": 0, "installerIcon": 7172, - "userIcon": 7172 + "userIcon": 7172, + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 35, @@ -694,5 +694,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 7407607e086e6..3b1278da0b998 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -2803,5 +2803,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index f892eb5570eee..272f6118830e5 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -64,5 +64,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index 8de7dd2b713e4..e652653d9461b 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -702,5 +702,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 0 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f93ba4fbb9395..1596b099ab1ad 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -6,6 +6,7 @@ import pytest from zwave_js_server.const import ( + InclusionState, InclusionStrategy, LogLevel, Protocols, @@ -77,14 +78,16 @@ async def test_network_status(hass, integration, hass_ws_client): entry = integration ws_client = await hass_ws_client(hass) - await ws_client.send_json( - {ID: 2, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} - ) - msg = await ws_client.receive_json() - result = msg["result"] + with patch("zwave_js_server.model.controller.Controller.async_get_state"): + await ws_client.send_json( + {ID: 2, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + result = msg["result"] assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" assert result["client"]["server_version"] == "1.0.0" + assert result["controller"]["inclusion_state"] == InclusionState.IDLE # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -168,6 +171,7 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): assert result["status"] == 1 assert result["zwave_plus_version"] == 1 assert result["highest_security_class"] == SecurityClass.S0_LEGACY + assert not result["is_controller_node"] # Test getting non-existent node fails await ws_client.send_json( @@ -474,7 +478,15 @@ async def test_add_node( event = Event( type="interview failed", - data={"source": "node", "event": "interview failed", "nodeId": 67}, + data={ + "source": "node", + "event": "interview failed", + "nodeId": 67, + "args": { + "errorMessage": "error", + "isFinal": True, + }, + }, ) client.driver.receive_event(event) @@ -1606,7 +1618,15 @@ async def test_replace_failed_node( event = Event( type="interview failed", - data={"source": "node", "event": "interview failed", "nodeId": 67}, + data={ + "source": "node", + "event": "interview failed", + "nodeId": 67, + "args": { + "errorMessage": "error", + "isFinal": True, + }, + }, ) client.driver.receive_event(event) @@ -2189,7 +2209,15 @@ async def test_refresh_node_info( event = Event( type="interview failed", - data={"source": "node", "event": "interview failed", "nodeId": 52}, + data={ + "source": "node", + "event": "interview failed", + "nodeId": 52, + "args": { + "errorMessage": "error", + "isFinal": True, + }, + }, ) client.driver.receive_event(event) diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py new file mode 100644 index 0000000000000..29858e0eb9761 --- /dev/null +++ b/tests/components/zwave_js/test_button.py @@ -0,0 +1,59 @@ +"""Test the Z-Wave JS button entities.""" +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity_registry import async_get + + +async def test_ping_entity( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, + caplog, +): + """Test ping entity.""" + client.async_send_command.return_value = {"responded": True} + + # Test successful ping call + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.z_wave_thermostat_ping", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.ping" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) + + client.async_send_command.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: "button.z_wave_thermostat_ping", + }, + blocking=True, + ) + + assert "There is no value to refresh for this entity" in caplog.text + + # Assert a node ping button entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + async_get(hass).async_get_entity_id( + DOMAIN, "sensor", f"{get_valueless_base_unique_id(client, node)}.ping" + ) + is None + ) diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 5377d420268c1..07663ce945692 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -67,7 +67,7 @@ async def test_get_actions( "device_id": device.id, "parameter": 3, "bitmask": None, - "subtype": f"{node.node_id}-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", }, ] actions = await async_get_device_automations( @@ -161,7 +161,7 @@ async def test_actions( "device_id": device.id, "parameter": 1, "bitmask": None, - "subtype": "2-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", "value": 1, }, }, @@ -328,7 +328,6 @@ async def test_get_action_capabilities( integration: ConfigEntry, ): """Test we get the expected action capabilities.""" - 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 @@ -423,7 +422,7 @@ async def test_get_action_capabilities( "type": "set_config_parameter", "parameter": 1, "bitmask": None, - "subtype": f"{node.node_id}-112-0-1 (Temperature Reporting Threshold)", + "subtype": "1 (Temperature Reporting Threshold)", }, ) assert capabilities and "extra_fields" in capabilities @@ -455,7 +454,7 @@ async def test_get_action_capabilities( "type": "set_config_parameter", "parameter": 10, "bitmask": None, - "subtype": f"{node.node_id}-112-0-10 (Temperature Reporting Filter)", + "subtype": "10 (Temperature Reporting Filter)", }, ) assert capabilities and "extra_fields" in capabilities @@ -482,7 +481,7 @@ async def test_get_action_capabilities( "type": "set_config_parameter", "parameter": 2, "bitmask": None, - "subtype": f"{node.node_id}-112-0-2 (HVAC Settings)", + "subtype": "2 (HVAC Settings)", }, ) assert not capabilities diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 71a6865287c55..2161c8e6fe4ec 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -52,7 +52,7 @@ async def test_get_conditions(hass, client, lock_schlage_be469, integration) -> "type": "config_parameter", "device_id": device.id, "value_id": value_id, - "subtype": f"{value_id} ({name})", + "subtype": f"{config_value.property_} ({name})", }, { "condition": "device", @@ -215,17 +215,6 @@ async def test_node_status_state( assert len(calls) == 4 assert calls[3].data["some"] == "dead - event - test_event4" - event = Event( - "unknown", - data={ - "source": "node", - "event": "unknown", - "nodeId": lock_schlage_be469.node_id, - }, - ) - lock_schlage_be469.receive_event(event) - await hass.async_block_till_done() - async def test_config_parameter_state( hass, client, lock_schlage_be469, integration, calls @@ -250,7 +239,7 @@ async def test_config_parameter_state( "device_id": device.id, "type": "config_parameter", "value_id": f"{lock_schlage_be469.node_id}-112-0-3", - "subtype": f"{lock_schlage_be469.node_id}-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", "value": 255, } ], @@ -270,7 +259,7 @@ async def test_config_parameter_state( "device_id": device.id, "type": "config_parameter", "value_id": f"{lock_schlage_be469.node_id}-112-0-6", - "subtype": f"{lock_schlage_be469.node_id}-112-0-6 (User Slot Status)", + "subtype": "6 (User Slot Status)", "value": 1, } ], @@ -483,7 +472,7 @@ async def test_get_condition_capabilities_config_parameter( "device_id": device.id, "type": "config_parameter", "value_id": f"{node.node_id}-112-0-1", - "subtype": f"{node.node_id}-112-0-1 (Temperature Reporting Threshold)", + "subtype": "1 (Temperature Reporting Threshold)", }, ) assert capabilities and "extra_fields" in capabilities @@ -514,7 +503,7 @@ async def test_get_condition_capabilities_config_parameter( "device_id": device.id, "type": "config_parameter", "value_id": f"{node.node_id}-112-0-10", - "subtype": f"{node.node_id}-112-0-10 (Temperature Reporting Filter)", + "subtype": "10 (Temperature Reporting Filter)", }, ) assert capabilities and "extra_fields" in capabilities @@ -540,7 +529,7 @@ async def test_get_condition_capabilities_config_parameter( "device_id": device.id, "type": "config_parameter", "value_id": f"{node.node_id}-112-0-2", - "subtype": f"{node.node_id}-112-0-2 (HVAC Settings)", + "subtype": "2 (HVAC Settings)", }, ) assert not capabilities diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index bf3738a7fb3fd..8a79270646192 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -1120,7 +1120,6 @@ async def test_get_value_updated_config_parameter_triggers( hass, client, lock_schlage_be469, integration ): """Test we get the zwave_js.value_updated.config_parameter trigger from a zwave_js device.""" - node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] expected_trigger = { @@ -1132,7 +1131,7 @@ async def test_get_value_updated_config_parameter_triggers( "property_key": None, "endpoint": 0, "command_class": CommandClass.CONFIGURATION.value, - "subtype": f"{node.node_id}-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", } triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device.id @@ -1163,7 +1162,7 @@ async def test_if_value_updated_config_parameter_fires( "property_key": None, "endpoint": 0, "command_class": CommandClass.CONFIGURATION.value, - "subtype": f"{node.node_id}-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", "from": 255, }, "action": { @@ -1212,7 +1211,6 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( hass, client, lock_schlage_be469, integration ): """Test we get the expected capabilities from a range zwave_js.value_updated.config_parameter trigger.""" - node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1226,7 +1224,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( "property_key": None, "endpoint": 0, "command_class": CommandClass.CONFIGURATION.value, - "subtype": f"{node.node_id}-112-0-6 (User Slot Status)", + "subtype": "6 (User Slot Status)", }, ) assert capabilities and "extra_fields" in capabilities @@ -1255,7 +1253,6 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate hass, client, lock_schlage_be469, integration ): """Test we get the expected capabilities from an enumerated zwave_js.value_updated.config_parameter trigger.""" - node = lock_schlage_be469 dev_reg = async_get_dev_reg(hass) device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] capabilities = await device_trigger.async_get_trigger_capabilities( @@ -1269,7 +1266,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate "property_key": None, "endpoint": 0, "command_class": CommandClass.CONFIGURATION.value, - "subtype": f"{node.node_id}-112-0-3 (Beeper)", + "subtype": "3 (Beeper)", }, ) assert capabilities and "extra_fields" in capabilities diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index b41292a15fc1b..332c8c846353e 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -2,9 +2,7 @@ from unittest.mock import patch import pytest -from zwave_js_server.const import CommandClass from zwave_js_server.event import Event -from zwave_js_server.model.value import _get_value_id_from_dict, get_value_id from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics from homeassistant.components.zwave_js.helpers import get_device_id @@ -43,9 +41,6 @@ async def test_device_diagnostics( assert device # Update a value and ensure it is reflected in the node state - value_id = get_value_id( - multisensor_6, CommandClass.SENSOR_MULTILEVEL, PROPERTY_ULTRAVIOLET - ) event = Event( type="value updated", data={ @@ -75,18 +70,7 @@ async def test_device_diagnostics( "maxSchemaVersion": 0, } - # Assert that the data returned doesn't match the stale node state data - assert diagnostics_data["state"] != multisensor_6.data - - # Replace data for the value we updated and assert the new node data is the same - # as what's returned - updated_node_data = multisensor_6.data.copy() - for idx, value in enumerate(updated_node_data["values"]): - if _get_value_id_from_dict(multisensor_6, value) == value_id: - updated_node_data["values"][idx] = multisensor_6.values[ - value_id - ].data.copy() - assert diagnostics_data["state"] == updated_node_data + assert diagnostics_data["state"] == multisensor_6.data async def test_device_diagnostics_error(hass, integration): diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index d66cd52be4021..32859ae3c37db 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -1,4 +1,7 @@ """Test Z-Wave JS (value notification) events.""" +from unittest.mock import AsyncMock + +import pytest from zwave_js_server.const import CommandClass from zwave_js_server.event import Event @@ -259,3 +262,48 @@ async def test_value_updated(hass, vision_security_zl7432, integration, client): await hass.async_block_till_done() # We should only still have captured one event assert len(events) == 1 + + +async def test_power_level_notification(hass, hank_binary_switch, integration, client): + """Test power level notification events.""" + # just pick a random node to fake the notification event + node = hank_binary_switch + events = async_capture_events(hass, "zwave_js_notification") + + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": 7, + "ccId": 115, + "args": { + "commandClassName": "Powerlevel", + "commandClass": 115, + "testNodeId": 1, + "status": 0, + "acknowledgedFrames": 2, + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["command_class_name"] == "Power Level" + assert events[0].data["command_class"] == 115 + assert events[0].data["test_node_id"] == 1 + assert events[0].data["status"] == 0 + assert events[0].data["acknowledged_frames"] == 2 + + +async def test_unknown_notification(hass, hank_binary_switch, integration, client): + """Test behavior of unknown notification type events.""" + # just pick a random node to fake the notification event + node = hank_binary_switch + + # We emit the event directly so we can skip any validation and event handling + # by the lib. We will use a class that is guaranteed not to be recognized + notification_obj = AsyncMock() + notification_obj.node = node + with pytest.raises(TypeError): + node.emit("notification", {"notification": notification_obj}) diff --git a/tests/components/zwave_js/test_humidifier.py b/tests/components/zwave_js/test_humidifier.py new file mode 100644 index 0000000000000..6e76fe8d1643e --- /dev/null +++ b/tests/components/zwave_js/test_humidifier.py @@ -0,0 +1,1056 @@ +"""Test the Z-Wave JS humidifier platform.""" +from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.humidity_control import HumidityControlMode +from zwave_js_server.event import Event + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.humidifier.const import ( + ATTR_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DOMAIN as HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +from .common import DEHUMIDIFIER_ADC_T3000_ENTITY, HUMIDIFIER_ADC_T3000_ENTITY + + +async def test_humidifier(hass, client, climate_adc_t3000, integration): + """Test a humidity control command class entity.""" + + node = climate_adc_t3000 + state = hass.states.get(HUMIDIFIER_ADC_T3000_ENTITY) + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_DEVICE_CLASS] == HumidifierDeviceClass.HUMIDIFIER + assert state.attributes[ATTR_HUMIDITY] == 35 + assert state.attributes[ATTR_MIN_HUMIDITY] == 10 + assert state.attributes[ATTR_MAX_HUMIDITY] == 70 + + client.async_send_command.reset_mock() + + # Test setting humidity + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY, + ATTR_HUMIDITY: 41, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 1, + "commandClassName": "Humidity Control Setpoint", + "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Humidifier", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "%", + "min": 10, + "max": 70, + "ccSpecific": {"setpointType": 1}, + }, + "value": 35, + } + assert args["value"] == 41 + + client.async_send_command.reset_mock() + + # Test de-humidify mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.DEHUMIDIFY), + "prevValue": int(HumidityControlMode.HUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(HUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Test auto mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.HUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(HUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_ON + + client.async_send_command.reset_mock() + + # Test off mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.HUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(HUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Test turning off when device is previously humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.HUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.HUMIDIFY), + } + assert args["value"] == int(HumidityControlMode.OFF) + + client.async_send_command.reset_mock() + + # Test turning off when device is previously auto + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.AUTO), + } + assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) + + client.async_send_command.reset_mock() + + # Test turning off when device is previously de-humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.DEHUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning off when device is previously off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.AUTO), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.HUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously auto + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously de-humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.DEHUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.DEHUMIDIFY), + } + assert args["value"] == int(HumidityControlMode.AUTO) + + client.async_send_command.reset_mock() + + # Test turning on when device is previously off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.AUTO), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.OFF), + } + assert args["value"] == int(HumidityControlMode.HUMIDIFY) + + +async def test_dehumidifier_missing_setpoint( + hass, client, climate_adc_t3000_missing_setpoint, integration +): + """Test a humidity control command class entity.""" + + entity_id = "humidifier.adc_t3000_missing_setpoint_dehumidifier" + state = hass.states.get(entity_id) + + assert state + assert ATTR_HUMIDITY not in state.attributes + assert state.attributes[ATTR_MIN_HUMIDITY] == DEFAULT_MIN_HUMIDITY + assert state.attributes[ATTR_MAX_HUMIDITY] == DEFAULT_MAX_HUMIDITY + + client.async_send_command.reset_mock() + + # Test setting humidity + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + ATTR_ENTITY_ID: entity_id, + ATTR_HUMIDITY: 41, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + +async def test_humidifier_missing_mode( + hass, client, climate_adc_t3000_missing_mode, integration +): + """Test a humidity control command class entity.""" + + node = climate_adc_t3000_missing_mode + + # Test that de-humidifer entity does not exist but humidifier entity does + entity_id = "humidifier.adc_t3000_missing_mode_dehumidifier" + state = hass.states.get(entity_id) + assert not state + + entity_id = "humidifier.adc_t3000_missing_mode_humidifier" + state = hass.states.get(entity_id) + assert state + + client.async_send_command.reset_mock() + + # Test turning off when device is previously auto for a device which does not have de-humidify mode + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.AUTO), + } + assert args["value"] == int(HumidityControlMode.OFF) + + client.async_send_command.reset_mock() + + +async def test_dehumidifier(hass, client, climate_adc_t3000, integration): + """Test a humidity control command class entity.""" + + node = climate_adc_t3000 + state = hass.states.get(DEHUMIDIFIER_ADC_T3000_ENTITY) + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_DEVICE_CLASS] == HumidifierDeviceClass.DEHUMIDIFIER + assert state.attributes[ATTR_HUMIDITY] == 60 + assert state.attributes[ATTR_MIN_HUMIDITY] == 30 + assert state.attributes[ATTR_MAX_HUMIDITY] == 90 + + client.async_send_command.reset_mock() + + # Test setting humidity + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + { + ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY, + ATTR_HUMIDITY: 41, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 1, + "commandClassName": "Humidity Control Setpoint", + "commandClass": CommandClass.HUMIDITY_CONTROL_SETPOINT, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "De-humidifier", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "%", + "min": 30, + "max": 90, + "ccSpecific": {"setpointType": 2}, + }, + "value": 60, + } + assert args["value"] == 41 + + client.async_send_command.reset_mock() + + # Test humidify mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.HUMIDIFY), + "prevValue": int(HumidityControlMode.DEHUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(DEHUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Test auto mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.DEHUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(DEHUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_ON + + client.async_send_command.reset_mock() + + # Test off mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.DEHUMIDIFY), + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(DEHUMIDIFIER_ADC_T3000_ENTITY) + assert state.state == STATE_OFF + + client.async_send_command.reset_mock() + + # Test turning off when device is previously de-humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.DEHUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.DEHUMIDIFY), + } + assert args["value"] == int(HumidityControlMode.OFF) + + client.async_send_command.reset_mock() + + # Test turning off when device is previously auto + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.AUTO), + } + assert args["value"] == int(HumidityControlMode.HUMIDIFY) + + client.async_send_command.reset_mock() + + # Test turning off when device is previously humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.HUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning off when device is previously off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.AUTO), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously de-humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.DEHUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously auto + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.AUTO), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + client.async_send_command.reset_mock() + + # Test turning on when device is previously humidifying + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.HUMIDIFY), + "prevValue": int(HumidityControlMode.OFF), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.HUMIDIFY), + } + assert args["value"] == int(HumidityControlMode.AUTO) + + client.async_send_command.reset_mock() + + # Test turning on when device is previously off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "newValue": int(HumidityControlMode.OFF), + "prevValue": int(HumidityControlMode.AUTO), + }, + }, + ) + node.receive_event(event) + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEHUMIDIFIER_ADC_T3000_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 2, + "commandClassName": "Humidity Control Mode", + "commandClass": CommandClass.HUMIDITY_CONTROL_MODE, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Humidity control mode", + "states": {"0": "Off", "1": "Humidify", "2": "De-humidify", "3": "Auto"}, + }, + "value": int(HumidityControlMode.OFF), + } + assert args["value"] == int(HumidityControlMode.DEHUMIDIFY) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 7e39b7845337e..1b3a2d1204f41 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -196,19 +196,23 @@ async def test_on_node_added_not_ready( assert len(hass.states.async_all()) == 0 assert not dev_reg.devices + node_state = deepcopy(zp3111_not_ready_state) + node_state["isSecure"] = False + event = Event( type="node added", data={ "source": "controller", "event": "node added", - "node": deepcopy(zp3111_not_ready_state), + "node": node_state, + "result": {}, }, ) client.driver.receive_event(event) await hass.async_block_till_done() - # the only entity is the node status sensor - assert len(hass.states.async_all()) == 1 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -250,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati assert not device.model assert not device.sw_version - # the only entity is the node status sensor - assert len(hass.states.async_all()) == 1 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -317,12 +321,16 @@ async def test_existing_node_not_replaced_when_not_ready( assert state.name == "Custom Entity Name" assert not hass.states.get(motion_entity) + node_state = deepcopy(zp3111_not_ready_state) + node_state["isSecure"] = False + event = Event( type="node added", data={ "source": "controller", "event": "node added", - "node": deepcopy(zp3111_not_ready_state), + "node": node_state, + "result": {}, }, ) client.driver.receive_event(event) @@ -788,10 +796,10 @@ async def test_remove_entry( assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text -async def test_removed_device(hass, client, multiple_devices, integration): +async def test_removed_device( + hass, client, climate_radio_thermostat_ct100_plus, lock_schlage_be469, integration +): """Test that the device registry gets updated when a device gets removed.""" - nodes = multiple_devices - # Verify how many nodes are available assert len(client.driver.controller.nodes) == 2 @@ -803,10 +811,10 @@ async def test_removed_device(hass, client, multiple_devices, integration): # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 26 + assert len(entity_entries) == 28 # Remove a node and reload the entry - old_node = nodes.pop(13) + old_node = client.driver.controller.nodes.pop(13) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -815,7 +823,7 @@ async def test_removed_device(hass, client, multiple_devices, integration): device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 16 + assert len(entity_entries) == 17 assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None @@ -838,9 +846,14 @@ async def test_node_removed(hass, multisensor_6_state, client, integration): dev_reg = dr.async_get(hass) node = Node(client, deepcopy(multisensor_6_state)) device_id = f"{client.driver.controller.home_id}-{node.node_id}" - event = {"node": node} + event = { + "source": "controller", + "event": "node added", + "node": node.data, + "result": {}, + } - client.driver.controller.emit("node added", event) + client.driver.controller.receive_event(Event("node added", event)) await hass.async_block_till_done() old_device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert old_device.id @@ -907,7 +920,7 @@ async def test_replace_same_node( "index": 0, "status": 4, "ready": False, - "isSecure": "unknown", + "isSecure": False, "interviewAttempts": 1, "endpoints": [{"nodeId": node_id, "index": 0, "deviceClass": None}], "values": [], @@ -921,7 +934,9 @@ async def test_replace_same_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, + "result": {}, }, ) @@ -1022,7 +1037,7 @@ async def test_replace_different_node( "index": 0, "status": 4, "ready": False, - "isSecure": "unknown", + "isSecure": False, "interviewAttempts": 1, "endpoints": [ {"nodeId": multisensor_6.node_id, "index": 0, "deviceClass": None} @@ -1038,7 +1053,9 @@ async def test_replace_different_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, + "result": {}, }, ) diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 373ca2525accc..01de5a70692ac 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -13,9 +13,17 @@ ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, SUPPORT_TRANSITION, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) from .common import ( AEON_SMART_SWITCH_LIGHT_ENTITY, @@ -24,6 +32,8 @@ ZEN_31_ENTITY, ) +HSM200_V1_ENTITY = "light.hsm200" + async def test_light(hass, client, bulb_6_multi_color, integration): """Test the light entity.""" @@ -62,7 +72,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -95,7 +104,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -168,7 +176,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -206,7 +213,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -444,7 +450,6 @@ async def test_light(hass, client, bulb_6_multi_color, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, } @@ -469,7 +474,6 @@ async def test_optional_light(hass, client, aeon_smart_switch_6, integration): async def test_rgbw_light(hass, client, zen_31, integration): """Test the light entity.""" - zen_31 state = hass.states.get(ZEN_31_ENTITY) assert state @@ -523,7 +527,6 @@ async def test_rgbw_light(hass, client, zen_31, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", "valueChangeOptions": ["transitionDuration"], }, "value": 59, @@ -542,3 +545,161 @@ async def test_light_none_color_value(hass, light_color_null_values, integration assert state.state == STATE_ON assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + + +async def test_black_is_off(hass, client, express_controls_ezmultipli, integration): + """Test the black is off light entity.""" + node = express_controls_ezmultipli + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_ON + + # Attempt to turn on the light and ensure it defaults to white + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == {"red": 255, "green": 255, "blue": 255} + + client.async_send_command.reset_mock() + + # Force the light to turn off + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_OFF + + # Force the light to turn on + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": { + "red": 0, + "green": 255, + "blue": 0, + }, + "prevValue": { + "red": 0, + "green": 0, + "blue": 0, + }, + "propertyName": "currentColor", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + state = hass.states.get(HSM200_V1_ENTITY) + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == {"red": 0, "green": 0, "blue": 0} + + client.async_send_command.reset_mock() + + # Assert that the last color is restored + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: HSM200_V1_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node.node_id + assert args["valueId"] == { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "label": "Target Color", + "type": "any", + "readable": True, + "writeable": True, + "valueChangeOptions": ["transitionDuration"], + }, + } + assert args["value"] == {"red": 0, "green": 255, "blue": 0} + + client.async_send_command.reset_mock() diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index ff3712b607e7a..95f969a9586f2 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -8,6 +8,7 @@ from homeassistant.components.zwave_js.api import ENTRY_ID, ID, TYPE from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.const import LIGHT_LUX from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR @@ -33,6 +34,10 @@ ZWAVE_MULTISENSOR_DEVICE_AREA = "Z-Wave Multisensor Area" ZWAVE_SOURCE_NODE_ENTITY = "sensor.zwave_source_node" ZWAVE_SOURCE_NODE_UNIQUE_ID = "52-4321" +ZWAVE_LUMINANCE_ENTITY = "sensor.zwave_luminance" +ZWAVE_LUMINANCE_UNIQUE_ID = "52-6543" +ZWAVE_LUMINANCE_NAME = "Z-Wave Luminance" +ZWAVE_LUMINANCE_ICON = "mdi:zwave-test-luminance" ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" ZWAVE_BATTERY_UNIQUE_ID = "52-1234" ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" @@ -69,6 +74,14 @@ def zwave_migration_data_fixture(hass): platform="zwave", name="Z-Wave Source Node", ) + zwave_luminance_entry = er.RegistryEntry( + entity_id=ZWAVE_LUMINANCE_ENTITY, + unique_id=ZWAVE_LUMINANCE_UNIQUE_ID, + platform="zwave", + name=ZWAVE_LUMINANCE_NAME, + icon=ZWAVE_LUMINANCE_ICON, + unit_of_measurement="lux", + ) zwave_battery_entry = er.RegistryEntry( entity_id=ZWAVE_BATTERY_ENTITY, unique_id=ZWAVE_BATTERY_UNIQUE_ID, @@ -131,6 +144,18 @@ def zwave_migration_data_fixture(hass): "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, "unit_of_measurement": zwave_source_node_entry.unit_of_measurement, }, + ZWAVE_LUMINANCE_ENTITY: { + "node_id": 52, + "node_instance": 1, + "command_class": 49, + "command_class_label": "Luminance", + "value_index": 3, + "device_id": zwave_multisensor_device.id, + "domain": zwave_luminance_entry.domain, + "entity_id": zwave_luminance_entry.entity_id, + "unique_id": ZWAVE_LUMINANCE_UNIQUE_ID, + "unit_of_measurement": zwave_luminance_entry.unit_of_measurement, + }, ZWAVE_BATTERY_ENTITY: { "node_id": 52, "node_instance": 1, @@ -169,6 +194,7 @@ def zwave_migration_data_fixture(hass): { ZWAVE_SWITCH_ENTITY: zwave_switch_entry, ZWAVE_SOURCE_NODE_ENTITY: zwave_source_node_entry, + ZWAVE_LUMINANCE_ENTITY: zwave_luminance_entry, ZWAVE_BATTERY_ENTITY: zwave_battery_entry, ZWAVE_POWER_ENTITY: zwave_power_entry, ZWAVE_TAMPERING_ENTITY: zwave_tampering_entry, @@ -218,6 +244,7 @@ async def test_migrate_zwave( migration_entity_map = { ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6", + ZWAVE_LUMINANCE_ENTITY: "sensor.multisensor_6_illuminance", ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level", } @@ -225,6 +252,7 @@ async def test_migrate_zwave( ZWAVE_SWITCH_ENTITY, ZWAVE_POWER_ENTITY, ZWAVE_SOURCE_NODE_ENTITY, + ZWAVE_LUMINANCE_ENTITY, ZWAVE_BATTERY_ENTITY, ZWAVE_TAMPERING_ENTITY, ] @@ -279,6 +307,7 @@ async def test_migrate_zwave( # this should have been migrated and no longer present under that id assert not ent_reg.async_is_registered("sensor.multisensor_6_battery_level") + assert not ent_reg.async_is_registered("sensor.multisensor_6_illuminance") # these should not have been migrated and is still in the registry assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY) @@ -295,6 +324,7 @@ async def test_migrate_zwave( # this is the new entity_ids of the zwave_js entities assert ent_reg.async_is_registered(ZWAVE_SWITCH_ENTITY) assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + assert ent_reg.async_is_registered(ZWAVE_LUMINANCE_ENTITY) # check that the migrated entries have correct attributes switch_entry = ent_reg.async_get(ZWAVE_SWITCH_ENTITY) @@ -307,6 +337,12 @@ async def test_migrate_zwave( assert battery_entry.unique_id == "3245146787.52-128-0-level" assert battery_entry.name == ZWAVE_BATTERY_NAME assert battery_entry.icon == ZWAVE_BATTERY_ICON + luminance_entry = ent_reg.async_get(ZWAVE_LUMINANCE_ENTITY) + assert luminance_entry + assert luminance_entry.unique_id == "3245146787.52-49-0-Illuminance" + assert luminance_entry.name == ZWAVE_LUMINANCE_NAME + assert luminance_entry.icon == ZWAVE_LUMINANCE_ICON + assert luminance_entry.unit_of_measurement == LIGHT_LUX # check that the zwave config entry has been removed assert not hass.config_entries.async_entries("zwave") @@ -317,6 +353,7 @@ async def test_migrate_zwave( assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id) +@pytest.mark.skip(reason="The old zwave integration has been disabled.") async def test_migrate_zwave_dry_run( hass, zwave_integration, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 2d120411513ca..1d41e145a95dc 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -15,8 +15,10 @@ ATTR_METER_TYPE_NAME, ATTR_VALUE, DOMAIN, + SERVICE_REFRESH_VALUE, SERVICE_RESET_METER, ) +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -154,7 +156,9 @@ 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, lock_id_lock_as_id150, integration): +async def test_node_status_sensor( + hass, client, controller_node, 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 @@ -200,6 +204,18 @@ async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integrati await client.disconnect() assert hass.states.get(NODE_STATUS_ENTITY).state != STATE_UNAVAILABLE + # Assert a node status sensor entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + ent_reg.async_get_entity_id( + DOMAIN, + "sensor", + f"{get_valueless_base_unique_id(client, node)}.node_status", + ) + is None + ) + async def test_node_status_sensor_not_ready( hass, @@ -207,6 +223,7 @@ async def test_node_status_sensor_not_ready( lock_id_lock_as_id150_not_ready, lock_id_lock_as_id150_state, integration, + caplog, ): """Test node status sensor is created and available if node is not ready.""" NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status" @@ -220,12 +237,31 @@ async def test_node_status_sensor_not_ready( assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" # Mark node as ready - event = Event("ready", {"nodeState": lock_id_lock_as_id150_state}) + event = Event( + "ready", + { + "source": "node", + "event": "ready", + "nodeId": node.node_id, + "nodeState": lock_id_lock_as_id150_state, + }, + ) node.receive_event(event) assert node.ready assert hass.states.get(NODE_STATUS_ENTITY) assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_VALUE, + { + ATTR_ENTITY_ID: NODE_STATUS_ENTITY, + }, + blocking=True, + ) + + assert "There is no value to refresh for this entity" in caplog.text + async def test_reset_meter( hass, diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 33f6205c7b986..5dbeff87a541a 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -264,6 +264,440 @@ def clear_events(): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) +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] + + 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") + controller_no_event_data_filter = async_capture_events( + hass, "controller_no_event_data_filter" + ) + controller_event_data_filter = async_capture_events( + hass, "controller_event_data_filter" + ) + driver_no_event_data_filter = async_capture_events( + hass, "driver_no_event_data_filter" + ) + driver_event_data_filter = async_capture_events(hass, "driver_event_data_filter") + node_event_data_no_partial_dict_match_filter = async_capture_events( + hass, "node_event_data_no_partial_dict_match_filter" + ) + node_event_data_partial_dict_match_filter = async_capture_events( + hass, "node_event_data_partial_dict_match_filter" + ) + + def clear_events(): + """Clear all events in the event list.""" + node_no_event_data_filter.clear() + node_event_data_filter.clear() + controller_no_event_data_filter.clear() + controller_event_data_filter.clear() + driver_no_event_data_filter.clear() + driver_event_data_filter.clear() + node_event_data_no_partial_dict_match_filter.clear() + node_event_data_partial_dict_match_filter.clear() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # node filter: no event data + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "interview stage completed", + }, + "action": { + "event": "node_no_event_data_filter", + }, + }, + # node filter: event data + { + "trigger": { + "platform": trigger_type, + "device_id": device.id, + "event_source": "node", + "event": "interview stage completed", + "event_data": {"stageName": "ProtocolInfo"}, + }, + "action": { + "event": "node_event_data_filter", + }, + }, + # controller filter: no event data + { + "trigger": { + "platform": trigger_type, + "config_entry_id": integration.entry_id, + "event_source": "controller", + "event": "inclusion started", + }, + "action": { + "event": "controller_no_event_data_filter", + }, + }, + # controller filter: event data + { + "trigger": { + "platform": trigger_type, + "config_entry_id": integration.entry_id, + "event_source": "controller", + "event": "inclusion started", + "event_data": {"secure": True}, + }, + "action": { + "event": "controller_event_data_filter", + }, + }, + # driver filter: no event data + { + "trigger": { + "platform": trigger_type, + "config_entry_id": integration.entry_id, + "event_source": "driver", + "event": "logging", + }, + "action": { + "event": "driver_no_event_data_filter", + }, + }, + # driver filter: event data + { + "trigger": { + "platform": trigger_type, + "config_entry_id": integration.entry_id, + "event_source": "driver", + "event": "logging", + "event_data": {"message": "test"}, + }, + "action": { + "event": "driver_event_data_filter", + }, + }, + # node filter: event data, no partial dict match + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "value updated", + "event_data": {"args": {"commandClassName": "Door Lock"}}, + }, + "action": { + "event": "node_event_data_no_partial_dict_match_filter", + }, + }, + # node filter: event data, partial dict match + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "value updated", + "event_data": {"args": {"commandClassName": "Door Lock"}}, + "partial_dict_match": True, + }, + "action": { + "event": "node_event_data_partial_dict_match_filter", + }, + }, + ] + }, + ) + + # Test that `node no event data filter` is triggered and `node event data filter` is not + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 1 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that `node no event data filter` and `node event data filter` are triggered + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "ProtocolInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 1 + assert len(node_event_data_filter) == 1 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that `controller no event data filter` is triggered and `controller event data filter` is not + event = Event( + type="inclusion started", + data={ + "source": "controller", + "event": "inclusion started", + "secure": False, + }, + ) + client.driver.controller.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 1 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that both `controller no event data filter` and `controller event data filter` are triggered + event = Event( + type="inclusion started", + data={ + "source": "controller", + "event": "inclusion started", + "secure": True, + }, + ) + client.driver.controller.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 1 + assert len(controller_event_data_filter) == 1 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that `driver no event data filter` is triggered and `driver event data filter` is not + event = Event( + type="logging", + data={ + "source": "driver", + "event": "logging", + "message": "no test", + "formattedMessage": "test", + "direction": ">", + "level": "debug", + "primaryTags": "tag", + "secondaryTags": "tag2", + "secondaryTagPadding": 0, + "multiline": False, + "timestamp": "time", + "label": "label", + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 1 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that both `driver no event data filter` and `driver event data filter` are triggered + event = Event( + type="logging", + data={ + "source": "driver", + "event": "logging", + "message": "test", + "formattedMessage": "test", + "direction": ">", + "level": "debug", + "primaryTags": "tag", + "secondaryTags": "tag2", + "secondaryTagPadding": 0, + "multiline": False, + "timestamp": "time", + "label": "label", + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 1 + assert len(driver_event_data_filter) == 1 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + # Test that only `node with event data and partial match dict filter` is triggered + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 49, + "endpoint": 0, + "property": "latchStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 1 + + clear_events() + + # Test that `node with event data and partial match dict filter` is not triggered + # when partial dict doesn't match + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "fake command class name", + "commandClass": 49, + "endpoint": 0, + "property": "latchStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + assert len(node_event_data_filter) == 0 + assert len(controller_no_event_data_filter) == 0 + assert len(controller_event_data_filter) == 0 + assert len(driver_no_event_data_filter) == 0 + assert len(driver_event_data_filter) == 0 + assert len(node_event_data_no_partial_dict_match_filter) == 0 + assert len(node_event_data_partial_dict_match_filter) == 0 + + clear_events() + + with patch("homeassistant.config.load_yaml", return_value={}): + await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) + + +async def test_zwave_js_event_invalid_config_entry_id( + hass, client, integration, caplog +): + """Test zwave_js.event automation trigger fails when config entry ID is invalid.""" + trigger_type = f"{DOMAIN}.event" + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": trigger_type, + "config_entry_id": "not_real_entry_id", + "event_source": "controller", + "event": "inclusion started", + }, + "action": { + "event": "node_no_event_data_filter", + }, + } + ] + }, + ) + + assert "Config entry 'not_real_entry_id' not found" in caplog.text + caplog.clear() + + +async def test_zwave_js_event_unloaded_config_entry(hass, client, integration, caplog): + """Test zwave_js.event automation trigger fails when config entry is unloaded.""" + trigger_type = f"{DOMAIN}.event" + + await hass.config_entries.async_unload(integration.entry_id) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": trigger_type, + "config_entry_id": integration.entry_id, + "event_source": "controller", + "event": "inclusion started", + }, + "action": { + "event": "node_no_event_data_filter", + }, + } + ] + }, + ) + + assert f"Config entry '{integration.entry_id}' not loaded" in caplog.text + + async def test_async_validate_trigger_config(hass): """Test async_validate_trigger_config.""" mock_platform = AsyncMock() diff --git a/tests/components/zwave_me/__init__.py b/tests/components/zwave_me/__init__.py new file mode 100644 index 0000000000000..da2457db55e02 --- /dev/null +++ b/tests/components/zwave_me/__init__.py @@ -0,0 +1 @@ +"""Tests for the zwave_me integration.""" diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py new file mode 100644 index 0000000000000..36a73c4fc064a --- /dev/null +++ b/tests/components/zwave_me/test_config_flow.py @@ -0,0 +1,185 @@ +"""Test the zwave_me config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.components.zwave_me.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, + FlowResult, +) + +from tests.common import MockConfigEntry + +MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( + host="ws://192.168.1.14", + hostname="mock_hostname", + name="mock_name", + addresses=["192.168.1.14"], + port=1234, + properties={ + "deviceid": "aa:bb:cc:dd:ee:ff", + "manufacturer": "fake_manufacturer", + "model": "fake_model", + "serialNumber": "fake_serial", + }, + type="mock_type", +) + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + with patch( + "homeassistant.components.zwave_me.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "192.168.1.14", + "token": "test-token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ws://192.168.1.14" + assert result2["data"] == { + "url": "ws://192.168.1.14", + "token": "test-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf(hass: HomeAssistant): + """Test starting a flow from zeroconf.""" + with patch( + "homeassistant.components.zwave_me.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ): + result: FlowResult = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "token": "test-token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "ws://192.168.1.14" + assert result2["data"] == { + "url": "ws://192.168.1.14", + "token": "test-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_error_handling_zeroconf(hass: HomeAssistant): + """Test getting proper errors from no uuid.""" + with patch("homeassistant.components.zwave_me.helpers.get_uuid", return_value=None): + result: FlowResult = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "no_valid_uuid_set" + + +async def test_handle_error_user(hass: HomeAssistant): + """Test getting proper errors from no uuid.""" + with patch("homeassistant.components.zwave_me.helpers.get_uuid", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "192.168.1.15", + "token": "test-token", + }, + ) + assert result2["errors"] == {"base": "no_valid_uuid_set"} + + +async def test_duplicate_user(hass: HomeAssistant): + """Test getting proper errors from duplicate uuid.""" + entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, + title="ZWave_me", + data={ + "url": "ws://192.168.1.15", + "token": "test-token", + }, + unique_id="test_uuid", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "192.168.1.15", + "token": "test-token", + }, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_duplicate_zeroconf(hass: HomeAssistant): + """Test getting proper errors from duplicate uuid.""" + entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, + title="ZWave_me", + data={ + "url": "ws://192.168.1.14", + "token": "test-token", + }, + unique_id="test_uuid", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ): + + result: FlowResult = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/conftest.py b/tests/conftest.py index 9f0958e6aced3..baac9ac19eed9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,6 +38,7 @@ from tests.common import ( # noqa: E402, isort:skip CLIENT_ID, INSTANCES, + MockConfigEntry, MockUser, async_fire_mqtt_message, async_test_home_assistant, @@ -590,19 +591,25 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): if mqtt_config is None: mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} - result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: mqtt_config}) - assert result await hass.async_block_till_done() - # Workaround: asynctest==0.13 fails on @functools.lru_cache - spec = dir(hass.data["mqtt"]) - spec.remove("_matching_subscriptions") + entry = MockConfigEntry( + data=mqtt_config, + domain=mqtt.DOMAIN, + title="Tasmota", + ) + + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_component_mock = MagicMock( return_value=hass.data["mqtt"], - spec_set=spec, + spec_set=hass.data["mqtt"], wraps=hass.data["mqtt"], ) + mqtt_component_mock.conf = hass.data["mqtt"].conf # For diagnostics mqtt_component_mock._mqttc = mqtt_client_mock hass.data["mqtt"] = mqtt_component_mock diff --git a/tests/fixtures/sleepiq-bed-single.json b/tests/fixtures/sleepiq-bed-single.json deleted file mode 100644 index 512f36c0e6afd..0000000000000 --- a/tests/fixtures/sleepiq-bed-single.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "beds" : [ - { - "dualSleep" : false, - "base" : "FlexFit", - "sku" : "AILE", - "model" : "ILE", - "size" : "KING", - "isKidsBed" : false, - "sleeperRightId" : "-80", - "accountId" : "-32", - "bedId" : "-31", - "registrationDate" : "2016-07-22T14:00:58Z", - "serial" : null, - "reference" : "95000794555-1", - "macAddress" : "CD13A384BA51", - "version" : null, - "purchaseDate" : "2016-06-22T00:00:00Z", - "sleeperLeftId" : "0", - "zipcode" : "12345", - "returnRequestStatus" : 0, - "name" : "ILE", - "status" : 1, - "timezone" : "US/Eastern" - } - ] -} diff --git a/tests/fixtures/sleepiq-bed.json b/tests/fixtures/sleepiq-bed.json deleted file mode 100644 index d03fb6e329f87..0000000000000 --- a/tests/fixtures/sleepiq-bed.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "beds" : [ - { - "dualSleep" : true, - "base" : "FlexFit", - "sku" : "AILE", - "model" : "ILE", - "size" : "KING", - "isKidsBed" : false, - "sleeperRightId" : "-80", - "accountId" : "-32", - "bedId" : "-31", - "registrationDate" : "2016-07-22T14:00:58Z", - "serial" : null, - "reference" : "95000794555-1", - "macAddress" : "CD13A384BA51", - "version" : null, - "purchaseDate" : "2016-06-22T00:00:00Z", - "sleeperLeftId" : "-92", - "zipcode" : "12345", - "returnRequestStatus" : 0, - "name" : "ILE", - "status" : 1, - "timezone" : "US/Eastern" - } - ] -} - diff --git a/tests/fixtures/sleepiq-familystatus-single.json b/tests/fixtures/sleepiq-familystatus-single.json deleted file mode 100644 index 08c9569c4dc71..0000000000000 --- a/tests/fixtures/sleepiq-familystatus-single.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "beds" : [ - { - "bedId" : "-31", - "rightSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "isInBed" : true, - "sleepNumber" : 40, - "alertDetailedMessage" : "No Alert", - "pressure" : -16 - }, - "status" : 1, - "leftSide" : null - } - ] -} diff --git a/tests/fixtures/sleepiq-familystatus.json b/tests/fixtures/sleepiq-familystatus.json deleted file mode 100644 index 0c93d74d35fa7..0000000000000 --- a/tests/fixtures/sleepiq-familystatus.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "beds" : [ - { - "bedId" : "-31", - "rightSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "isInBed" : true, - "sleepNumber" : 40, - "alertDetailedMessage" : "No Alert", - "pressure" : -16 - }, - "status" : 1, - "leftSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "sleepNumber" : 80, - "alertDetailedMessage" : "No Alert", - "isInBed" : false, - "pressure" : 2191 - } - } - ] -} diff --git a/tests/fixtures/sleepiq-login-failed.json b/tests/fixtures/sleepiq-login-failed.json deleted file mode 100644 index 227609154b5b7..0000000000000 --- a/tests/fixtures/sleepiq-login-failed.json +++ /dev/null @@ -1 +0,0 @@ -{"Error":{"Code":401,"Message":"Authentication token of type [class org.apache.shiro.authc.UsernamePasswordToken] could not be authenticated by any configured realms. Please ensure that at least one realm can authenticate these tokens."}} diff --git a/tests/fixtures/sleepiq-login.json b/tests/fixtures/sleepiq-login.json deleted file mode 100644 index fdd8943574f80..0000000000000 --- a/tests/fixtures/sleepiq-login.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "edpLoginStatus" : 200, - "userId" : "-42", - "registrationState" : 13, - "key" : "0987", - "edpLoginMessage" : "not used" -} diff --git a/tests/fixtures/sleepiq-sleeper.json b/tests/fixtures/sleepiq-sleeper.json deleted file mode 100644 index 4089e1b1d95e2..0000000000000 --- a/tests/fixtures/sleepiq-sleeper.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "sleepers" : [ - { - "timezone" : "US/Eastern", - "firstName" : "Test1", - "weight" : 150, - "birthMonth" : 12, - "birthYear" : "1990", - "active" : true, - "lastLogin" : "2016-08-26 21:43:27 CDT", - "side" : 1, - "accountId" : "-32", - "height" : 60, - "bedId" : "-31", - "username" : "test1@example.com", - "sleeperId" : "-80", - "avatar" : "", - "emailValidated" : true, - "licenseVersion" : 6, - "duration" : null, - "email" : "test1@example.com", - "isAccountOwner" : true, - "sleepGoal" : 480, - "zipCode" : "12345", - "isChild" : false, - "isMale" : true - }, - { - "email" : "test2@example.com", - "duration" : null, - "emailValidated" : true, - "licenseVersion" : 5, - "isChild" : false, - "isMale" : false, - "zipCode" : "12345", - "isAccountOwner" : false, - "sleepGoal" : 480, - "side" : 0, - "lastLogin" : "2016-07-17 15:37:30 CDT", - "birthMonth" : 1, - "birthYear" : "1991", - "active" : true, - "weight" : 151, - "firstName" : "Test2", - "timezone" : "US/Eastern", - "avatar" : "", - "username" : "test2@example.com", - "sleeperId" : "-92", - "bedId" : "-31", - "height" : 65, - "accountId" : "-32" - } - ] -} - diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index d958c633b621f..ffbc2130f3b30 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -46,8 +46,11 @@ def setup_comp(hass): ) +@pytest.fixture(autouse=True) def teardown(): """Restore.""" + yield + dt_util.set_default_time_zone(ORIG_TIME_ZONE) @@ -1810,54 +1813,51 @@ async def test_extract_entities(): async def test_extract_devices(): """Test extracting devices.""" - assert ( - condition.async_extract_devices( - { - "condition": "and", - "conditions": [ - {"condition": "device", "device_id": "abcd", "domain": "light"}, - {"condition": "device", "device_id": "qwer", "domain": "switch"}, - { - "condition": "state", - "entity_id": "sensor.not_a_device", - "state": "100", - }, - { - "condition": "not", - "conditions": [ - { - "condition": "device", - "device_id": "abcd_not", - "domain": "light", - }, - { - "condition": "device", - "device_id": "qwer_not", - "domain": "switch", - }, - ], - }, - { - "condition": "or", - "conditions": [ - { - "condition": "device", - "device_id": "abcd_or", - "domain": "light", - }, - { - "condition": "device", - "device_id": "qwer_or", - "domain": "switch", - }, - ], - }, - Template("{{ is_state('light.example', 'on') }}"), - ], - } - ) - == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} - ) + assert condition.async_extract_devices( + { + "condition": "and", + "conditions": [ + {"condition": "device", "device_id": "abcd", "domain": "light"}, + {"condition": "device", "device_id": "qwer", "domain": "switch"}, + { + "condition": "state", + "entity_id": "sensor.not_a_device", + "state": "100", + }, + { + "condition": "not", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_not", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_not", + "domain": "switch", + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_or", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_or", + "domain": "switch", + }, + ], + }, + Template("{{ is_state('light.example', 'on') }}"), + ], + } + ) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} async def test_condition_template_error(hass): @@ -2977,7 +2977,7 @@ async def test_platform_async_validate_condition_config(hass): config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"} platform = AsyncMock() with patch( - "homeassistant.helpers.condition.async_get_device_automation_platform", + "homeassistant.components.device_automation.condition.async_get_device_automation_platform", return_value=platform, ): platform.async_validate_condition_config.return_value = config diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 62ae79ec5cc49..daa8d11d60133 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -410,10 +410,16 @@ def test_service_schema(): "entity_id": "all", "alias": "turn on kitchen lights", }, + {"service": "scene.turn_on", "metadata": {}}, ) for value in options: cv.SERVICE_SCHEMA(value) + # Check metadata is removed from the validated output + assert cv.SERVICE_SCHEMA({"service": "scene.turn_on", "metadata": {}}) == { + "service": "scene.turn_on" + } + def test_entity_service_schema(): """Test make_entity_service_schema validation.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index a0949bad03ce0..4e4150fe504cc 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -96,8 +96,12 @@ async def test_get_or_create_returns_same_entry( assert len(update_events) == 2 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "connections": {("mac", "12:34:56:ab:cd:ef")} + } async def test_requirement_for_identifier_or_connection(registry): @@ -518,14 +522,19 @@ async def test_removing_config_entries(hass, registry, update_events): assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id + assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] async def test_deleted_device_removing_config_entries(hass, registry, update_events): @@ -568,14 +577,19 @@ async def test_deleted_device_removing_config_entries(hass, registry, update_eve assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2]["device_id"] assert update_events[3]["action"] == "remove" assert update_events[3]["device_id"] == entry.id + assert "changes" not in update_events[3] assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] registry.async_clear_config_entry("123") assert len(registry.devices) == 0 @@ -892,7 +906,7 @@ async def test_format_mac(registry): assert list(invalid_mac_entry.connections)[0][1] == invalid -async def test_update(registry): +async def test_update(hass, registry, update_events): """Verify that we can update some attributes of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -940,6 +954,24 @@ async def test_update(registry): assert registry.async_get(updated_entry.id) is not None + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "area_id": None, + "disabled_by": None, + "identifiers": {("bla", "123"), ("hue", "456")}, + "manufacturer": None, + "model": None, + "name_by_user": None, + "via_device_id": None, + } + async def test_update_remove_config_entries(hass, registry, update_events): """Make sure we do not get duplicate entries.""" @@ -989,17 +1021,22 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert len(update_events) == 5 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry2.id + assert update_events[1]["changes"] == {"config_entries": {"123"}} assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry3.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "update" assert update_events[3]["device_id"] == entry.id + assert update_events[3]["changes"] == {"config_entries": {"456", "123"}} assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry3.id + assert "changes" not in update_events[4] -async def test_update_sw_version(registry): +async def test_update_sw_version(hass, registry, update_events): """Verify that we can update software version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1016,8 +1053,18 @@ async def test_update_sw_version(registry): assert updated_entry != entry assert updated_entry.sw_version == sw_version + await hass.async_block_till_done() -async def test_update_hw_version(registry): + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"sw_version": None} + + +async def test_update_hw_version(hass, registry, update_events): """Verify that we can update hardware version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1034,8 +1081,18 @@ async def test_update_hw_version(registry): assert updated_entry != entry assert updated_entry.hw_version == hw_version + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"hw_version": None} + -async def test_update_suggested_area(registry, area_registry): +async def test_update_suggested_area(hass, registry, area_registry, update_events): """Verify that we can update the suggested area version of a device.""" entry = registry.async_get_or_create( config_entry_id="1234", @@ -1061,6 +1118,16 @@ async def test_update_suggested_area(registry, area_registry): assert updated_entry.area_id == pool_area.id assert len(area_registry.areas) == 1 + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == {"area_id": None, "suggested_area": None} + async def test_cleanup_device_registry(hass, registry): """Test cleanup works.""" @@ -1221,12 +1288,16 @@ async def test_restore_device(hass, registry, update_events): assert len(update_events) == 4 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "remove" assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[1] assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry2.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry3.id + assert "changes" not in update_events[3] async def test_restore_simple_device(hass, registry, update_events): @@ -1266,12 +1337,16 @@ async def test_restore_simple_device(hass, registry, update_events): assert len(update_events) == 4 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "remove" assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[1] assert update_events[2]["action"] == "create" assert update_events[2]["device_id"] == entry2.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry3.id + assert "changes" not in update_events[3] async def test_restore_shared_device(hass, registry, update_events): @@ -1358,18 +1433,31 @@ async def test_restore_shared_device(hass, registry, update_events): assert len(update_events) == 7 assert update_events[0]["action"] == "create" assert update_events[0]["device_id"] == entry.id + assert "changes" not in update_events[0] assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id + assert update_events[1]["changes"] == { + "config_entries": {"123"}, + "identifiers": {("entry_123", "0123")}, + } assert update_events[2]["action"] == "remove" assert update_events[2]["device_id"] == entry.id + assert "changes" not in update_events[2] assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry.id + assert "changes" not in update_events[3] assert update_events[4]["action"] == "remove" assert update_events[4]["device_id"] == entry.id + assert "changes" not in update_events[4] assert update_events[5]["action"] == "create" assert update_events[5]["device_id"] == entry.id - assert update_events[1]["action"] == "update" - assert update_events[1]["device_id"] == entry.id + assert "changes" not in update_events[5] + assert update_events[6]["action"] == "update" + assert update_events[6]["device_id"] == entry.id + assert update_events[6]["changes"] == { + "config_entries": {"234"}, + "identifiers": {("entry_234", "2345")}, + } async def test_get_or_create_empty_then_set_default_values(hass, registry): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 1eaac0e72bfb2..bad19bf78cbbc 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -9,6 +9,7 @@ from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory from tests.common import ( MockConfigEntry, @@ -77,7 +78,7 @@ def test_get_or_create_updates_data(registry): config_entry=orig_config_entry, device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, - entity_category="config", + entity_category=EntityCategory.CONFIG, original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", @@ -95,7 +96,7 @@ def test_get_or_create_updates_data(registry): device_class=None, device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, - entity_category="config", + entity_category=EntityCategory.CONFIG, icon=None, id=orig_entry.id, name=None, @@ -135,7 +136,7 @@ def test_get_or_create_updates_data(registry): device_class=None, device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated - entity_category="config", + entity_category=EntityCategory.CONFIG, icon=None, id=orig_entry.id, name=None, @@ -189,7 +190,7 @@ async def test_loading_saving_data(hass, registry): config_entry=mock_config, device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, - entity_category="config", + entity_category=EntityCategory.CONFIG, original_device_class="mock-device-class", original_icon="hass:original-icon", original_name="Original Name", @@ -809,6 +810,109 @@ async def test_remove_device_removes_entities(hass, registry): assert not registry.async_is_registered(entry.entity_id) +async def test_remove_config_entry_from_device_removes_entities(hass, registry): + """Test that we remove entities tied to a device when config entry is removed.""" + device_registry = mock_device_registry(hass) + config_entry_1 = MockConfigEntry(domain="hue") + config_entry_2 = MockConfigEntry(domain="device_tracker") + + # Create device with two config entries + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + } + + # Create one entity for each config entry + entry_1 = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry_1, + device_id=device_entry.id, + ) + + entry_2 = registry.async_get_or_create( + "sensor", + "device_tracker", + "6789", + config_entry=config_entry_2, + device_id=device_entry.id, + ) + + assert registry.async_is_registered(entry_1.entity_id) + assert registry.async_is_registered(entry_2.entity_id) + + # Remove the first config entry from the device, the entity associated with it + # should be removed + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=config_entry_1.entry_id + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert not registry.async_is_registered(entry_1.entity_id) + assert registry.async_is_registered(entry_2.entity_id) + + # Remove the second config entry from the device, the entity associated with it + # (and the device itself) should be removed + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=config_entry_2.entry_id + ) + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) + assert not registry.async_is_registered(entry_1.entity_id) + assert not registry.async_is_registered(entry_2.entity_id) + + +async def test_remove_config_entry_from_device_removes_entities_2(hass, registry): + """Test that we don't remove entities with no config entry when device is modified.""" + device_registry = mock_device_registry(hass) + config_entry_1 = MockConfigEntry(domain="hue") + config_entry_2 = MockConfigEntry(domain="device_tracker") + + # Create device with two config entries + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + } + + # Create one entity for each config entry + entry_1 = registry.async_get_or_create( + "light", + "hue", + "5678", + device_id=device_entry.id, + ) + + assert registry.async_is_registered(entry_1.entity_id) + + # Remove the first config entry from the device + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=config_entry_1.entry_id + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert registry.async_is_registered(entry_1.entity_id) + + async def test_update_device_race(hass, registry): """Test race when a device is created, updated and removed.""" device_registry = mock_device_registry(hass) @@ -1116,9 +1220,9 @@ def test_entity_registry_items(): async def test_deprecated_disabled_by_str(hass, registry, caplog): """Test deprecated str use of disabled_by converts to enum and logs a warning.""" entry = registry.async_get_or_create( - "light", - "hue", - "5678", + domain="light.kitchen", + platform="hue", + unique_id="5678", disabled_by=er.RegistryEntryDisabler.USER.value, ) @@ -1126,12 +1230,25 @@ async def test_deprecated_disabled_by_str(hass, registry, caplog): assert " str for entity registry disabled_by. This is deprecated " in caplog.text +async def test_deprecated_entity_category_str(hass, registry, caplog): + """Test deprecated str use of entity_category converts to enum and logs a warning.""" + entry = er.RegistryEntry( + entity_id="light.kitchen", + unique_id="5678", + platform="hue", + entity_category="diagnostic", + ) + + assert entry.entity_category is EntityCategory.DIAGNOSTIC + assert " should be updated to use EntityCategory" in caplog.text + + async def test_invalid_entity_category_str(hass, registry, caplog): """Test use of invalid entity category.""" entry = er.RegistryEntry( - "light", - "hue", - "5678", + entity_id="light.kitchen", + unique_id="5678", + platform="hue", entity_category="invalid", ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 4f62d50da34fe..bd17aec92e655 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -46,8 +46,11 @@ DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE +@pytest.fixture(autouse=True) def teardown(): """Stop everything that was started.""" + yield + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 37f3e7ec95f23..936940869d6b5 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -92,3 +92,16 @@ async def test_prevent_flooding(caplog): assert what not in caplog.text assert key in frame._REPORTED_INTEGRATIONS assert len(frame._REPORTED_INTEGRATIONS) == 1 + + +async def test_report_missing_integration_frame(caplog): + """Test reporting when no integration is detected.""" + + what = "teststring" + with patch( + "homeassistant.helpers.frame.get_integration_frame", + side_effect=frame.MissingIntegrationFrame, + ): + frame.report(what, error_if_core=False) + assert what in caplog.text + assert caplog.text.count(what) == 1 diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 219d015bdf7e8..5ae1891e45acf 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -1,5 +1,5 @@ """Tests Home Assistant location helpers.""" -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import State from homeassistant.helpers import location @@ -73,6 +73,21 @@ async def test_coordinates_function_device_tracker_in_zone(hass): ) +async def test_coordinates_function_zone_friendly_name(hass): + """Test coordinates function.""" + hass.states.async_set( + "zone.home", + "zoning", + {"latitude": 32.87336, "longitude": -117.22943, ATTR_FRIENDLY_NAME: "my_home"}, + ) + hass.states.async_set( + "test.object", + "my_home", + ) + assert location.find_coordinates(hass, "test.object") == "32.87336,-117.22943" + assert location.find_coordinates(hass, "my_home") == "32.87336,-117.22943" + + async def test_coordinates_function_device_tracker_from_input_select(hass): """Test coordinates function.""" hass.states.async_set( @@ -96,15 +111,16 @@ def test_coordinates_function_returns_none_on_recursion(hass): assert location.find_coordinates(hass, "test.first") is None -async def test_coordinates_function_returns_none_if_invalid_coord(hass): +async def test_coordinates_function_returns_state_if_no_coords(hass): """Test test_coordinates function.""" hass.states.async_set( "test.object", "abc", ) - assert location.find_coordinates(hass, "test.object") is None + assert location.find_coordinates(hass, "test.object") == "abc" -def test_coordinates_function_returns_none_if_invalid_input(hass): +def test_coordinates_function_returns_input_if_no_coords(hass): """Test test_coordinates function.""" - assert location.find_coordinates(hass, "test.abc") is None + assert location.find_coordinates(hass, "test.abc") == "test.abc" + assert location.find_coordinates(hass, "abc") == "abc" diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 05c72f10db568..0838375fd1f7e 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -13,12 +13,24 @@ _get_internal_url, _get_request_host, get_url, + is_hass_url, is_internal_request, ) from tests.common import mock_component +@pytest.fixture(name="mock_current_request") +def mock_current_request_mock(): + """Mock the current request.""" + mock_current_request = Mock(name="mock_request") + with patch( + "homeassistant.helpers.network.http.current_request", + Mock(get=mock_current_request), + ): + yield mock_current_request + + async def test_get_url_internal(hass: HomeAssistant): """Test getting an instance URL when the user has set an internal URL.""" assert hass.config.internal_url is None @@ -480,6 +492,12 @@ async def test_get_url(hass: HomeAssistant): get_url(hass, prefer_external=True, allow_external=False) == "http://example.local" ) + # Prefer external defaults to True if use_ssl=True + hass.config.api = Mock(use_ssl=True) + assert get_url(hass) == "https://example.com" + hass.config.api = Mock(use_ssl=False) + assert get_url(hass) == "http://example.local" + hass.config.api = None with pytest.raises(NoURLAvailableError): get_url(hass, allow_external=False, require_ssl=True) @@ -519,6 +537,19 @@ async def test_get_url(hass: HomeAssistant): ), pytest.raises(NoURLAvailableError): _get_internal_url(hass, require_current_request=True) + # Test allow_ip defaults when SSL specified + await async_process_ha_core_config( + hass, + {"external_url": "https://1.1.1.1"}, + ) + assert hass.config.external_url == "https://1.1.1.1" + assert get_url(hass, allow_internal=False) == "https://1.1.1.1" + hass.config.api = Mock(use_ssl=False) + assert get_url(hass, allow_internal=False) == "https://1.1.1.1" + hass.config.api = Mock(use_ssl=True) + with pytest.raises(NoURLAvailableError): + assert get_url(hass, allow_internal=False) + async def test_get_request_host(hass: HomeAssistant): """Test getting the host of the current web request from the request context.""" @@ -591,7 +622,7 @@ async def test_get_current_request_url_with_known_host( get_url(hass, require_current_request=True) -async def test_is_internal_request(hass: HomeAssistant): +async def test_is_internal_request(hass: HomeAssistant, mock_current_request): """Test if accessing an instance on its internal URL.""" # Test with internal URL: http://example.local:8123 await async_process_ha_core_config( @@ -600,18 +631,16 @@ async def test_is_internal_request(hass: HomeAssistant): ) assert hass.config.internal_url == "http://example.local:8123" + + # No request active + mock_current_request.return_value = None assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="example.local" - ): - assert is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://example.local:8123") + assert is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.local", - ): - assert not is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://no_match.example.local:8123") + assert not is_internal_request(hass) # Test with internal URL: http://192.168.0.1:8123 await async_process_ha_core_config( @@ -622,7 +651,67 @@ async def test_is_internal_request(hass: HomeAssistant): assert hass.config.internal_url == "http://192.168.0.1:8123" assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + mock_current_request.return_value = Mock(url="http://192.168.0.1:8123") + assert is_internal_request(hass) + + # Test for matching against local IP + hass.config.api = Mock(use_ssl=False, local_ip="192.168.123.123", port=8123) + for allowed in ("127.0.0.1", "192.168.123.123"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url + + # Test for matching against HassOS hostname + with patch.object( + hass.components.hassio, "is_hassio", return_value=True + ), patch.object( + hass.components.hassio, + "get_host_info", + return_value={"hostname": "hellohost"}, + ): + for allowed in ("hellohost", "hellohost.local"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url + + +async def test_is_hass_url(hass): + """Test is_hass_url.""" + assert hass.config.api is None + assert hass.config.internal_url is None + assert hass.config.external_url is None + + assert is_hass_url(hass, "http://example.com") is False + + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + assert is_hass_url(hass, "http://192.168.123.123:8123") is True + assert is_hass_url(hass, "https://192.168.123.123:8123") is False + assert is_hass_url(hass, "http://192.168.123.123") is False + + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + assert is_hass_url(hass, "http://example.local:8123") is True + assert is_hass_url(hass, "https://example.local:8123") is False + assert is_hass_url(hass, "http://example.local") is False + + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com:443"}, + ) + assert is_hass_url(hass, "https://example.com:443") is True + assert is_hass_url(hass, "https://example.com") is True + assert is_hass_url(hass, "http://example.com:443") is False + assert is_hass_url(hass, "http://example.com") is False + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + return_value="https://example.nabu.casa", ): - assert is_internal_request(hass) + assert is_hass_url(hass, "https://example.nabu.casa") is False + + hass.config.components.add("cloud") + assert is_hass_url(hass, "https://example.nabu.casa:443") is True + assert is_hass_url(hass, "https://example.nabu.casa") is True + assert is_hass_url(hass, "http://example.nabu.casa:443") is False + assert is_hass_url(hass, "http://example.nabu.casa") is False diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 79719b7532614..efe951342fa25 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -22,9 +22,9 @@ async def test_caching_data(hass): """Test that we cache data.""" now = dt_util.utcnow() stored_states = [ - StoredState(State("input_boolean.b0", "on"), now), - StoredState(State("input_boolean.b1", "on"), now), - StoredState(State("input_boolean.b2", "on"), now), + StoredState(State("input_boolean.b0", "on"), None, now), + StoredState(State("input_boolean.b1", "on"), None, now), + StoredState(State("input_boolean.b2", "on"), None, now), ] data = await RestoreStateData.async_get_instance(hass) @@ -160,9 +160,9 @@ async def test_hass_starting(hass): now = dt_util.utcnow() stored_states = [ - StoredState(State("input_boolean.b0", "on"), now), - StoredState(State("input_boolean.b1", "on"), now), - StoredState(State("input_boolean.b2", "on"), now), + StoredState(State("input_boolean.b0", "on"), None, now), + StoredState(State("input_boolean.b1", "on"), None, now), + StoredState(State("input_boolean.b2", "on"), None, now), ] data = await RestoreStateData.async_get_instance(hass) @@ -225,15 +225,16 @@ async def test_dump_data(hass): data = await RestoreStateData.async_get_instance(hass) now = dt_util.utcnow() data.last_states = { - "input_boolean.b0": StoredState(State("input_boolean.b0", "off"), now), - "input_boolean.b1": StoredState(State("input_boolean.b1", "off"), now), - "input_boolean.b2": StoredState(State("input_boolean.b2", "off"), now), - "input_boolean.b3": StoredState(State("input_boolean.b3", "off"), now), + "input_boolean.b0": StoredState(State("input_boolean.b0", "off"), None, now), + "input_boolean.b1": StoredState(State("input_boolean.b1", "off"), None, now), + "input_boolean.b2": StoredState(State("input_boolean.b2", "off"), None, now), + "input_boolean.b3": StoredState(State("input_boolean.b3", "off"), None, now), "input_boolean.b4": StoredState( State("input_boolean.b4", "off"), + None, datetime(1985, 10, 26, 1, 22, tzinfo=dt_util.UTC), ), - "input_boolean.b5": StoredState(State("input_boolean.b5", "off"), now), + "input_boolean.b5": StoredState(State("input_boolean.b5", "off"), None, now), } with patch( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 5bb4833a796e2..11ba9810b9dff 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3721,7 +3721,7 @@ async def test_platform_async_validate_action_config(hass): config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test"} platform = AsyncMock() with patch( - "homeassistant.helpers.script.device_automation.async_get_device_automation_platform", + "homeassistant.components.device_automation.action.async_get_device_automation_platform", return_value=platform, ): platform.async_validate_action_config.return_value = config diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 23d8200be23c7..edf856d68432b 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -2,7 +2,10 @@ import pytest import voluptuous as vol -from homeassistant.helpers import selector +from homeassistant.helpers import config_validation as cv, selector +from homeassistant.util import dt as dt_util + +FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @pytest.mark.parametrize( @@ -20,6 +23,8 @@ def test_valid_base_schema(schema): @pytest.mark.parametrize( "schema", ( + None, + "not_a_dict", {}, {"non_existing": {}}, # Two keys @@ -38,173 +43,268 @@ def test_validate_selector(): assert schema == selector.validate_selector(schema) +def _test_selector( + selector_type, schema, valid_selections, invalid_selections, converter=None +): + """Help test a selector.""" + + def default_converter(x): + return x + + if converter is None: + converter = default_converter + + # Validate selector configuration + selector.validate_selector({selector_type: schema}) + + # Use selector in schema and validate + vol_schema = vol.Schema({"selection": selector.selector({selector_type: schema})}) + for selection in valid_selections: + assert vol_schema({"selection": selection}) == { + "selection": converter(selection) + } + for selection in invalid_selections: + with pytest.raises(vol.Invalid): + vol_schema({"selection": selection}) + + # Serialize selector + selector_instance = selector.selector({selector_type: schema}) + assert cv.custom_serializer(selector_instance) == { + "selector": {selector_type: selector_instance.config} + } + + @pytest.mark.parametrize( - "schema", + "schema,valid_selections,invalid_selections", ( - {}, - {"integration": "zha"}, - {"manufacturer": "mock-manuf"}, - {"model": "mock-model"}, - {"manufacturer": "mock-manuf", "model": "mock-model"}, - {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"}, - {"entity": {"device_class": "motion"}}, - { - "integration": "zha", - "manufacturer": "mock-manuf", - "model": "mock-model", - "entity": {"domain": "binary_sensor", "device_class": "motion"}, - }, + (None, ("abc123",), (None,)), + ({}, ("abc123",), (None,)), + ({"integration": "zha"}, ("abc123",), (None,)), + ({"manufacturer": "mock-manuf"}, ("abc123",), (None,)), + ({"model": "mock-model"}, ("abc123",), (None,)), + ({"manufacturer": "mock-manuf", "model": "mock-model"}, ("abc123",), (None,)), + ( + {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"}, + ("abc123",), + (None,), + ), + ({"entity": {"device_class": "motion"}}, ("abc123",), (None,)), + ( + { + "integration": "zha", + "manufacturer": "mock-manuf", + "model": "mock-model", + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + }, + ("abc123",), + (None,), + ), ), ) -def test_device_selector_schema(schema): +def test_device_selector_schema(schema, valid_selections, invalid_selections): """Test device selector.""" - selector.validate_selector({"device": schema}) + _test_selector("device", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", + "schema,valid_selections,invalid_selections", ( - {}, - {"integration": "zha"}, - {"domain": "light"}, - {"device_class": "motion"}, - {"integration": "zha", "domain": "light"}, - {"integration": "zha", "domain": "binary_sensor", "device_class": "motion"}, + ({}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), + ({"integration": "zha"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), + ({"domain": "light"}, ("light.abc123", FAKE_UUID), (None, "sensor.abc123")), + ({"device_class": "motion"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), + ( + {"integration": "zha", "domain": "light"}, + ("light.abc123", FAKE_UUID), + (None, "sensor.abc123"), + ), + ( + {"integration": "zha", "domain": "binary_sensor", "device_class": "motion"}, + ("binary_sensor.abc123", FAKE_UUID), + (None, "sensor.abc123"), + ), ), ) -def test_entity_selector_schema(schema): +def test_entity_selector_schema(schema, valid_selections, invalid_selections): """Test entity selector.""" - selector.validate_selector({"entity": schema}) + _test_selector("entity", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", + "schema,valid_selections,invalid_selections", ( - {}, - {"entity": {}}, - {"entity": {"domain": "light"}}, - {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, - { - "entity": { - "domain": "binary_sensor", - "device_class": "motion", - "integration": "demo", - } - }, - {"device": {"integration": "demo", "model": "mock-model"}}, - { - "entity": {"domain": "binary_sensor", "device_class": "motion"}, - "device": {"integration": "demo", "model": "mock-model"}, - }, + ({}, ("abc123",), (None,)), + ({"entity": {}}, ("abc123",), (None,)), + ({"entity": {"domain": "light"}}, ("abc123",), (None,)), + ( + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + ("abc123",), + (None,), + ), + ( + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + ("abc123",), + (None,), + ), + ( + {"device": {"integration": "demo", "model": "mock-model"}}, + ("abc123",), + (None,), + ), + ( + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + ("abc123",), + (None,), + ), ), ) -def test_area_selector_schema(schema): +def test_area_selector_schema(schema, valid_selections, invalid_selections): """Test area selector.""" - selector.validate_selector({"area": schema}) + _test_selector("area", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", + "schema,valid_selections,invalid_selections", ( - {"min": 10, "max": 50}, - {"min": -100, "max": 100, "step": 5}, - {"min": -20, "max": -10, "mode": "box"}, - {"min": 0, "max": 100, "unit_of_measurement": "seconds", "mode": "slider"}, - {"min": 10, "max": 1000, "mode": "slider", "step": 0.5}, + ( + {"min": 10, "max": 50}, + ( + 10, + 50, + ), + (9, 51), + ), + ({"min": -100, "max": 100, "step": 5}, (), ()), + ({"min": -20, "max": -10, "mode": "box"}, (), ()), + ( + {"min": 0, "max": 100, "unit_of_measurement": "seconds", "mode": "slider"}, + (), + (), + ), + ({"min": 10, "max": 1000, "mode": "slider", "step": 0.5}, (), ()), ), ) -def test_number_selector_schema(schema): +def test_number_selector_schema(schema, valid_selections, invalid_selections): """Test number selector.""" - selector.validate_selector({"number": schema}) + _test_selector("number", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({},), + "schema,valid_selections,invalid_selections", + (({}, ("abc123",), (None,)),), ) -def test_addon_selector_schema(schema): +def test_addon_selector_schema(schema, valid_selections, invalid_selections): """Test add-on selector.""" - selector.validate_selector({"addon": schema}) + _test_selector("addon", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({},), + "schema,valid_selections,invalid_selections", + (({}, (1, "one", None), ()),), # Everything can be coarced to bool ) -def test_boolean_selector_schema(schema): +def test_boolean_selector_schema(schema, valid_selections, invalid_selections): """Test boolean selector.""" - selector.validate_selector({"boolean": schema}) + _test_selector("boolean", schema, valid_selections, invalid_selections, bool) @pytest.mark.parametrize( - "schema", - ({},), + "schema,valid_selections,invalid_selections", + (({}, ("00:00:00",), ("blah", None)),), ) -def test_time_selector_schema(schema): +def test_time_selector_schema(schema, valid_selections, invalid_selections): """Test time selector.""" - selector.validate_selector({"time": schema}) + _test_selector( + "time", schema, valid_selections, invalid_selections, dt_util.parse_time + ) @pytest.mark.parametrize( - "schema", + "schema,valid_selections,invalid_selections", ( - {}, - {"entity": {}}, - {"entity": {"domain": "light"}}, - {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, - { - "entity": { - "domain": "binary_sensor", - "device_class": "motion", - "integration": "demo", - } - }, - {"device": {"integration": "demo", "model": "mock-model"}}, - { - "entity": {"domain": "binary_sensor", "device_class": "motion"}, - "device": {"integration": "demo", "model": "mock-model"}, - }, + ({}, ({"entity_id": ["sensor.abc123"]},), ("abc123", None)), + ({"entity": {}}, (), ()), + ({"entity": {"domain": "light"}}, (), ()), + ({"entity": {"domain": "binary_sensor", "device_class": "motion"}}, (), ()), + ( + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + (), + (), + ), + ({"device": {"integration": "demo", "model": "mock-model"}}, (), ()), + ( + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + (), + (), + ), ), ) -def test_target_selector_schema(schema): +def test_target_selector_schema(schema, valid_selections, invalid_selections): """Test target selector.""" - selector.validate_selector({"target": schema}) + _test_selector("target", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({},), + "schema,valid_selections,invalid_selections", + (({}, ("abc123",), ()),), ) -def test_action_selector_schema(schema): +def test_action_selector_schema(schema, valid_selections, invalid_selections): """Test action sequence selector.""" - selector.validate_selector({"action": schema}) + _test_selector("action", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({},), + "schema,valid_selections,invalid_selections", + (({}, ("abc123",), ()),), ) -def test_object_selector_schema(schema): +def test_object_selector_schema(schema, valid_selections, invalid_selections): """Test object selector.""" - selector.validate_selector({"object": schema}) + _test_selector("object", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({}, {"multiline": True}, {"multiline": False}), + "schema,valid_selections,invalid_selections", + ( + ({}, ("abc123",), (None,)), + ({"multiline": True}, (), ()), + ({"multiline": False}, (), ()), + ), ) -def test_text_selector_schema(schema): +def test_text_selector_schema(schema, valid_selections, invalid_selections): """Test text selector.""" - selector.validate_selector({"text": schema}) + _test_selector("text", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( - "schema", - ({"options": ["red", "green", "blue"]},), + "schema,valid_selections,invalid_selections", + ( + ( + {"options": ["red", "green", "blue"]}, + ("red", "green", "blue"), + ("cat", 0, None), + ), + ), ) -def test_select_selector_schema(schema): +def test_select_selector_schema(schema, valid_selections, invalid_selections): """Test select selector.""" - selector.validate_selector({"select": schema}) + _test_selector("select", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 42834b3c14963..d70837bd08834 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3182,12 +3182,12 @@ def test_render_complex_handling_non_template_values(hass): def test_urlencode(hass): """Test the urlencode method.""" tpl = template.Template( - ("{% set dict = {'foo': 'x&y', 'bar': 42} %}" "{{ dict | urlencode }}"), + ("{% set dict = {'foo': 'x&y', 'bar': 42} %}{{ dict | urlencode }}"), hass, ) assert tpl.async_render() == "foo=x%26y&bar=42" tpl = template.Template( - ("{% set string = 'the quick brown fox = true' %}" "{{ string | urlencode }}"), + ("{% set string = 'the quick brown fox = true' %}{{ string | urlencode }}"), hass, ) assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true" diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 5565b43a78e13..89c70eaf83cf9 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -1,7 +1,9 @@ """Mock helpers for Z-Wave component.""" from unittest.mock import MagicMock -from pydispatch import dispatcher +# Integration & integration tests are disabled +# from pydispatch import dispatcher +dispatcher = MagicMock() def value_changed(value): diff --git a/tests/pylint/__init__.py b/tests/pylint/__init__.py index d6bdd6675f0eb..e03a2d2a118f6 100644 --- a/tests/pylint/__init__.py +++ b/tests/pylint/__init__.py @@ -1 +1,31 @@ """Tests for pylint.""" +import contextlib + +from pylint.testutils.unittest_linter import UnittestLinter + + +@contextlib.contextmanager +def assert_no_messages(linter: UnittestLinter): + """Assert that no messages are added by the given method.""" + with assert_adds_messages(linter): + yield + + +@contextlib.contextmanager +def assert_adds_messages(linter: UnittestLinter, *messages): + """Assert that exactly the given method adds the given messages. + + The list of messages must exactly match *all* the messages added by the + method. Additionally, we check to see whether the args in each message can + actually be substituted into the message string. + """ + yield + got = linter.release_messages() + no_msg = "No message." + expected = "\n".join(repr(m) for m in messages) or no_msg + got_str = "\n".join(repr(m) for m in got) or no_msg + msg = ( + "Expected messages did not match actual.\n" + f"\nExpected:\n{expected}\n\nGot:\n{got_str}\n" + ) + assert got == list(messages), msg diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py new file mode 100644 index 0000000000000..887f50fb628ac --- /dev/null +++ b/tests/pylint/conftest.py @@ -0,0 +1,30 @@ +"""Configuration for pylint tests.""" +from importlib.machinery import SourceFileLoader +from types import ModuleType + +from pylint.checkers import BaseChecker +from pylint.testutils.unittest_linter import UnittestLinter +import pytest + + +@pytest.fixture(name="hass_enforce_type_hints") +def hass_enforce_type_hints_fixture() -> ModuleType: + """Fixture to provide a requests mocker.""" + loader = SourceFileLoader( + "hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py" + ) + return loader.load_module(None) + + +@pytest.fixture(name="linter") +def linter_fixture() -> UnittestLinter: + """Fixture to provide a requests mocker.""" + return UnittestLinter() + + +@pytest.fixture(name="type_hint_checker") +def type_hint_checker_fixture(hass_enforce_type_hints, linter) -> BaseChecker: + """Fixture to provide a requests mocker.""" + type_hint_checker = hass_enforce_type_hints.HassTypeHintChecker(linter) + type_hint_checker.module = "homeassistant.components.pylint_test" + return type_hint_checker diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index fe60ed022f48e..81fdd2fa916bb 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1,16 +1,17 @@ """Tests for pylint hass_enforce_type_hints plugin.""" # pylint:disable=protected-access -from importlib.machinery import SourceFileLoader import re +from types import ModuleType +from unittest.mock import patch +import astroid +from pylint.checkers import BaseChecker +import pylint.testutils +from pylint.testutils.unittest_linter import UnittestLinter import pytest -loader = SourceFileLoader( - "hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py" -) -hass_enforce_type_hints = loader.load_module(None) -_TYPE_HINT_MATCHERS: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS +from . import assert_adds_messages, assert_no_messages @pytest.mark.parametrize( @@ -20,9 +21,17 @@ ("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"), ], ) -def test_regex_x_of_y_comma_z(string, expected_x, expected_y, expected_z): +def test_regex_x_of_y_comma_z( + hass_enforce_type_hints: ModuleType, + string: str, + expected_x: str, + expected_y: str, + expected_z: str, +) -> None: """Test x_of_y_comma_z regexes.""" - assert (match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(string)) + matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS + + assert (match := matchers["x_of_y_comma_z"].match(string)) assert match.group(0) == string assert match.group(1) == expected_x assert match.group(2) == expected_y @@ -33,9 +42,122 @@ def test_regex_x_of_y_comma_z(string, expected_x, expected_y, expected_z): ("string", "expected_a", "expected_b"), [("DiscoveryInfoType | None", "DiscoveryInfoType", "None")], ) -def test_regex_a_or_b(string, expected_a, expected_b): +def test_regex_a_or_b( + hass_enforce_type_hints: ModuleType, string: str, expected_a: str, expected_b: str +) -> None: """Test a_or_b regexes.""" - assert (match := _TYPE_HINT_MATCHERS["a_or_b"].match(string)) + matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS + + assert (match := matchers["a_or_b"].match(string)) assert match.group(0) == string assert match.group(1) == expected_a assert match.group(2) == expected_b + + +@pytest.mark.parametrize( + "code", + [ + """ + async def setup( #@ + arg1, arg2 + ): + pass + """ + ], +) +def test_ignore_not_annotations( + hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str +) -> None: + """Ensure that _is_valid_type is not run if there are no annotations.""" + func_node = astroid.extract_node(code) + + with patch.object( + hass_enforce_type_hints, "_is_valid_type", return_value=True + ) as is_valid_type: + type_hint_checker.visit_asyncfunctiondef(func_node) + is_valid_type.assert_not_called() + + +@pytest.mark.parametrize( + "code", + [ + """ + async def setup( #@ + arg1: ArgHint, arg2 + ): + pass + """, + """ + async def setup( #@ + arg1, arg2 + ) -> ReturnHint: + pass + """, + """ + async def setup( #@ + arg1: ArgHint, arg2: ArgHint + ) -> ReturnHint: + pass + """, + ], +) +def test_dont_ignore_partial_annotations( + hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str +) -> None: + """Ensure that _is_valid_type is run if there is at least one annotation.""" + func_node = astroid.extract_node(code) + + with patch.object( + hass_enforce_type_hints, "_is_valid_type", return_value=True + ) as is_valid_type: + type_hint_checker.visit_asyncfunctiondef(func_node) + is_valid_type.assert_called() + + +def test_invalid_discovery_info( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for discovery_info.""" + type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker" + func_node, discovery_info_node = astroid.extract_node( + """ + async def async_setup_scanner( #@ + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: dict[str, Any] | None = None, #@ + ) -> bool: + pass + """ + ) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=discovery_info_node, + args=(4, "DiscoveryInfoType | None"), + ), + ): + type_hint_checker.visit_asyncfunctiondef(func_node) + + +def test_valid_discovery_info( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for discovery_info.""" + type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker" + func_node = astroid.extract_node( + """ + async def async_setup_scanner( #@ + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: DiscoveryInfoType | None = None, + ) -> bool: + pass + """ + ) + + with assert_no_messages(linter): + type_hint_checker.visit_asyncfunctiondef(func_node) diff --git a/tests/test_config.py b/tests/test_config.py index 41e9bc5003803..4e761bc3f4706 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -53,8 +53,11 @@ def create_file(path): pass +@pytest.fixture(autouse=True) def teardown(): """Clean up.""" + yield + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE if os.path.isfile(YAML_PATH): @@ -78,6 +81,11 @@ def teardown(): async def test_create_default_config(hass): """Test creation of default config.""" + assert not os.path.isfile(YAML_PATH) + assert not os.path.isfile(SECRET_PATH) + assert not os.path.isfile(VERSION_PATH) + assert not os.path.isfile(AUTOMATIONS_PATH) + await config_util.async_create_default_config(hass) assert os.path.isfile(YAML_PATH) @@ -91,6 +99,7 @@ async def test_ensure_config_exists_creates_config(hass): If not creates a new config file. """ + assert not os.path.isfile(YAML_PATH) with patch("builtins.print") as mock_print: await config_util.async_ensure_config_exists(hass) @@ -1050,23 +1059,20 @@ async def test_component_config_exceptions(hass, caplog): # component.PLATFORM_SCHEMA caplog.clear() - assert ( - await config_util.async_process_component_config( - hass, - {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( - return_value=Mock( - spec=["PLATFORM_SCHEMA_BASE"], - PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), - ) - ), + assert await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock( + spec=["PLATFORM_SCHEMA_BASE"], + PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), + ) ), - ) - == {"test_domain": []} - ) + ), + ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( "Unknown error validating test_platform platform config with test_domain component platform schema" @@ -1085,20 +1091,15 @@ async def test_component_config_exceptions(hass, caplog): ) ), ): - assert ( - await config_util.async_process_component_config( - hass, - {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( - return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"]) - ), - ), - ) - == {"test_domain": []} - ) + assert await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + ), + ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index cad92a5d92d88..b62e9bffbce40 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2893,12 +2893,23 @@ async def test_setup_retrying_during_shutdown(hass): [ ({}, "already_configured"), ({"host": "3.3.3.3"}, "no_match"), + ({"vendor": "no_match"}, "no_match"), ({"host": "3.4.5.6"}, "already_configured"), ({"host": "3.4.5.6", "ip": "3.4.5.6"}, "no_match"), ({"host": "3.4.5.6", "ip": "1.2.3.4"}, "already_configured"), ({"host": "3.4.5.6", "ip": "1.2.3.4", "port": 23}, "already_configured"), + ( + {"host": "9.9.9.9", "ip": "6.6.6.6", "port": 12, "vendor": "zoo"}, + "already_configured", + ), + ({"vendor": "zoo"}, "already_configured"), ({"ip": "9.9.9.9"}, "already_configured"), ({"ip": "7.7.7.7"}, "no_match"), # ignored + ({"vendor": "data"}, "no_match"), + ( + {"vendor": "options"}, + "already_configured", + ), # ensure options takes precedence over data ], ) async def test__async_abort_entries_match(hass, manager, matchers, reason): @@ -2917,6 +2928,16 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): source=config_entries.SOURCE_IGNORE, data={"ip": "7.7.7.7", "host": "4.5.6.7", "port": 23}, ).add_to_hass(hass) + MockConfigEntry( + domain="comp", + data={"ip": "6.6.6.6", "host": "9.9.9.9", "port": 12}, + options={"vendor": "zoo"}, + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", + data={"vendor": "data"}, + options={"vendor": "options"}, + ).add_to_hass(hass) mock_setup_entry = AsyncMock(return_value=True) diff --git a/tests/test_core.py b/tests/test_core.py index c2d99967a4b4e..e052b08eccaa7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -39,6 +39,7 @@ ServiceNotFound, ) import homeassistant.util.dt as dt_util +from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import async_capture_events, async_mock_service @@ -48,7 +49,17 @@ def test_split_entity_id(): """Test split_entity_id.""" - assert ha.split_entity_id("domain.object_id") == ["domain", "object_id"] + assert ha.split_entity_id("domain.object_id") == ("domain", "object_id") + with pytest.raises(ValueError): + ha.split_entity_id("") + with pytest.raises(ValueError): + ha.split_entity_id(".") + with pytest.raises(ValueError): + ha.split_entity_id("just_domain") + with pytest.raises(ValueError): + ha.split_entity_id("empty_object_id.") + with pytest.raises(ValueError): + ha.split_entity_id(".empty_domain") def test_async_add_hass_job_schedule_callback(): @@ -377,10 +388,14 @@ def test_state_as_dict(): "last_updated": last_time.isoformat(), "state": "on", } - assert state.as_dict() == expected + as_dict_1 = state.as_dict() + assert isinstance(as_dict_1, ReadOnlyDict) + assert isinstance(as_dict_1["attributes"], ReadOnlyDict) + assert isinstance(as_dict_1["context"], ReadOnlyDict) + assert as_dict_1 == expected # 2nd time to verify cache assert state.as_dict() == expected - assert state.as_dict() is state.as_dict() + assert state.as_dict() is as_dict_1 async def test_eventbus_add_remove_listener(hass): diff --git a/tests/test_loader.py b/tests/test_loader.py index 68946a9de0123..9f2aaff58b7b8 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -203,6 +203,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, {"hostname": "tesla_*", "macaddress": "044EAF*"}, {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + {"registered_devices": True}, ], "usb": [ {"vid": "10C4", "pid": "EA60"}, @@ -233,6 +234,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, {"hostname": "tesla_*", "macaddress": "044EAF*"}, {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + {"registered_devices": True}, ] assert integration.usb == [ {"vid": "10C4", "pid": "EA60"}, @@ -615,3 +617,22 @@ async def test_validation(hass): """Test we raise if invalid domain passed in.""" with pytest.raises(ValueError): await loader.async_get_integration(hass, "some.thing") + + +async def test_loggers(hass): + """Test we can fetch the loggers from the integration.""" + name = "dummy" + integration = loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": True, + "dependencies": [], + "requirements": [], + "loggers": ["name1", "name2"], + }, + ) + assert integration.loggers == ["name1", "name2"] diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 31a8a5c71e325..6868ff1f71db3 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -21,7 +21,7 @@ def mock_stream(data): """Mock a stream with data.""" protocol = mock.Mock(_reading_paused=False) - stream = StreamReader(protocol, limit=2 ** 16) + stream = StreamReader(protocol, limit=2**16) stream.feed_data(data) stream.feed_eof() return stream diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 4ad2580ad8bc3..56587c80c3480 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -5,6 +5,7 @@ """ from homeassistant.components.sensor import ( DEVICE_CLASSES, + RestoreSensor, SensorDeviceClass, SensorEntity, ) @@ -109,3 +110,17 @@ def native_value(self): def state_class(self): """Return the state class of this sensor.""" return self._handle("state_class") + + +class MockRestoreSensor(MockSensor, RestoreSensor): + """Mock RestoreSensor class.""" + + async def async_added_to_hass(self) -> None: + """Restore native_value and native_unit_of_measurement.""" + await super().async_added_to_hass() + if (last_sensor_data := await self.async_get_last_sensor_data()) is None: + return + self._values["native_value"] = last_sensor_data.native_value + self._values[ + "native_unit_of_measurement" + ] = last_sensor_data.native_unit_of_measurement diff --git a/tests/util/test_async.py b/tests/util/test_async.py index f02d3c03b4b71..9bae6f5ebea09 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -105,8 +105,8 @@ async def test_check_loop_async_integration(caplog): ): hasync.check_loop(banned_function) assert ( - "Detected blocking call inside the event loop. This is causing stability issues. " - "Please report issue for hue doing blocking calls at " + "Detected blocking call to banned_function inside the event loop. This is " + "causing stability issues. Please report issue for hue doing blocking calls at " "homeassistant/components/hue/light.py, line 23: self.light.is_on" in caplog.text ) @@ -136,8 +136,8 @@ async def test_check_loop_async_integration_non_strict(caplog): ): hasync.check_loop(banned_function, strict=False) assert ( - "Detected blocking call inside the event loop. This is causing stability issues. " - "Please report issue for hue doing blocking calls at " + "Detected blocking call to banned_function inside the event loop. This is " + "causing stability issues. Please report issue for hue doing blocking calls at " "homeassistant/components/hue/light.py, line 23: self.light.is_on" in caplog.text ) @@ -167,9 +167,10 @@ async def test_check_loop_async_custom(caplog): ): hasync.check_loop(banned_function) assert ( - "Detected blocking call inside the event loop. This is causing stability issues. " - "Please report issue to the custom component author for hue doing blocking calls " - "at custom_components/hue/light.py, line 23: self.light.is_on" in caplog.text + "Detected blocking call to banned_function inside the event loop. This is " + "causing stability issues. Please report issue to the custom component author " + "for hue doing blocking calls at custom_components/hue/light.py, line 23: " + "self.light.is_on" in caplog.text ) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index eff71ddef4e32..b77540acc2be6 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -504,20 +504,17 @@ def test_rgbww_to_color_temperature(): Temperature values must be in mireds Home Assistant uses rgbcw for rgbww """ - assert ( - color_util.rgbww_to_color_temperature( - ( - 0, - 0, - 0, - 255, - 0, - ), - 153, - 500, - ) - == (153, 255) - ) + assert color_util.rgbww_to_color_temperature( + ( + 0, + 0, + 0, + 255, + 0, + ), + 153, + 500, + ) == (153, 255) assert color_util.rgbww_to_color_temperature((0, 0, 0, 128, 0), 153, 500) == ( 153, 128, @@ -550,15 +547,12 @@ def test_white_levels_to_color_temperature(): Temperature values must be in mireds Home Assistant uses rgbcw for rgbww """ - assert ( - color_util.while_levels_to_color_temperature( - 255, - 0, - 153, - 500, - ) - == (153, 255) - ) + assert color_util.while_levels_to_color_temperature( + 255, + 0, + 153, + 500, + ) == (153, 255) assert color_util.while_levels_to_color_temperature(128, 0, 153, 500) == ( 153, 128, diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 63513c9036002..d2c453f070da0 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -9,8 +9,11 @@ TEST_TIME_ZONE = "America/Los_Angeles" +@pytest.fixture(autouse=True) def teardown(): """Stop everything that was started.""" + yield + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index d885186871987..461d94d0c6712 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -26,14 +26,14 @@ TMP_DIR = None -def setup(): - """Set up for tests.""" +@pytest.fixture(autouse=True) +def setup_and_teardown(): + """Clean up after tests.""" global TMP_DIR TMP_DIR = mkdtemp() + yield -def teardown(): - """Clean up after tests.""" for fname in os.listdir(TMP_DIR): os.remove(os.path.join(TMP_DIR, fname)) os.rmdir(TMP_DIR) @@ -143,21 +143,15 @@ def default(self, o): bad_data = object() - assert ( - find_paths_unserializable_data( - [State("mock_domain.mock_entity", "on", {"bad": bad_data})], - dump=partial(dumps, cls=MockJSONEncoder), - ) - == {"$[0](State: mock_domain.mock_entity).attributes.bad": bad_data} - ) + assert find_paths_unserializable_data( + [State("mock_domain.mock_entity", "on", {"bad": bad_data})], + dump=partial(dumps, cls=MockJSONEncoder), + ) == {"$[0](State: mock_domain.mock_entity).attributes.bad": bad_data} - assert ( - find_paths_unserializable_data( - [Event("bad_event", {"bad_attribute": bad_data})], - dump=partial(dumps, cls=MockJSONEncoder), - ) - == {"$[0](Event: bad_event).data.bad_attribute": bad_data} - ) + assert find_paths_unserializable_data( + [Event("bad_event", {"bad_attribute": bad_data})], + dump=partial(dumps, cls=MockJSONEncoder), + ) == {"$[0](Event: bad_event).data.bad_attribute": bad_data} class BadData: def __init__(self): @@ -166,10 +160,7 @@ def __init__(self): def as_dict(self): return {"bla": self.bla} - assert ( - find_paths_unserializable_data( - BadData(), - dump=partial(dumps, cls=MockJSONEncoder), - ) - == {"$(BadData).bla": bad_data} - ) + assert find_paths_unserializable_data( + BadData(), + dump=partial(dumps, cls=MockJSONEncoder), + ) == {"$(BadData).bla": bad_data} diff --git a/tests/util/test_network.py b/tests/util/test_network.py index 089ef5e0ab8e8..b5c6b1a3e2417 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -56,6 +56,22 @@ def test_is_ip_address(): assert not network_util.is_ip_address("example.com") +def test_is_ipv4_address(): + """Test if strings are IPv4 addresses.""" + assert network_util.is_ipv4_address("192.168.0.1") is True + assert network_util.is_ipv4_address("8.8.8.8") is True + assert network_util.is_ipv4_address("192.168.0.999") is False + assert network_util.is_ipv4_address("192.168.0.0/24") is False + assert network_util.is_ipv4_address("example.com") is False + + +def test_is_ipv6_address(): + """Test if strings are IPv6 addresses.""" + assert network_util.is_ipv6_address("::1") is True + assert network_util.is_ipv6_address("8.8.8.8") is False + assert network_util.is_ipv6_address("8.8.8.8") is False + + def test_normalize_url(): """Test the normalizing of URLs.""" assert network_util.normalize_url("http://example.com") == "http://example.com" diff --git a/tests/util/test_read_only_dict.py b/tests/util/test_read_only_dict.py new file mode 100644 index 0000000000000..7528c843f5094 --- /dev/null +++ b/tests/util/test_read_only_dict.py @@ -0,0 +1,36 @@ +"""Test read only dictionary.""" +import json + +import pytest + +from homeassistant.util.read_only_dict import ReadOnlyDict + + +def test_read_only_dict(): + """Test read only dictionary.""" + data = ReadOnlyDict({"hello": "world"}) + + with pytest.raises(RuntimeError): + data["hello"] = "universe" + + with pytest.raises(RuntimeError): + data["other_key"] = "universe" + + with pytest.raises(RuntimeError): + data.pop("hello") + + with pytest.raises(RuntimeError): + data.popitem() + + with pytest.raises(RuntimeError): + data.clear() + + with pytest.raises(RuntimeError): + data.update({"yo": "yo"}) + + with pytest.raises(RuntimeError): + data.setdefault("yo", "yo") + + assert isinstance(data, dict) + assert dict(data) == {"hello": "world"} + assert json.dumps(data) == json.dumps({"hello": "world"}) diff --git a/tests/util/yaml/test_input.py b/tests/util/yaml/test_input.py index 1c13d1b36847a..fe118c79dbd0c 100644 --- a/tests/util/yaml/test_input.py +++ b/tests/util/yaml/test_input.py @@ -25,10 +25,7 @@ def test_substitute(): with pytest.raises(UndefinedSubstitution): substitute(Input("hello"), {}) - assert ( - substitute( - {"info": [1, Input("hello"), 2, Input("world")]}, - {"hello": 5, "world": 10}, - ) - == {"info": [1, 5, 2, 10]} - ) + assert substitute( + {"info": [1, Input("hello"), 2, Input("world")]}, + {"hello": 5, "world": 10}, + ) == {"info": [1, 5, 2, 10]} diff --git a/tox.ini b/tox.ini index af2f996195615..b47a6c94ac87f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,8 @@ ignore_basepython_conflict = True [testenv] basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=8.0.3,<20.3 +pip_version = pip>=21.0,<22.1 +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